|
85322
|
2920
|
40
|
2026-05-28T12:18:12.412042+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970692412_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","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\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"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,"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,"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,"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5244664462083781043
|
-1183062619475730196
|
typing_pause
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85321
|
2920
|
39
|
2026-05-28T12:18:08.961395+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970688961_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","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\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"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,"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,"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,"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,"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,"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8618909521242162662
|
-30141114868883220
|
typing_pause
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto...
|
85319
|
NULL
|
NULL
|
NULL
|
|
85320
|
2921
|
40
|
2026-05-28T12:18:07.140587+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970687140_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.37865692,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.39029256,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39993352,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40724733,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6731292379551279285
|
-7875264099512014368
|
typing_pause
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
rapstomEV faVsco,ls ~ProjectvViewCooc#12121 on JY-20963-fix-InWindowwnapers.oh© VoiceConsentPrefix.phpe Not iicationsnObservers© AccountObserver.php© ActivityObserver.php© ContactObserver.php© [EMAIL]© ProfileObserver.phpSocialAccountObserver.phpUserObserver.php© UserRoleObserver.php› Co PoliciesC ProvidersQueuea ReoositonieRutesvserviceActivityAireoorsEJ AvatarlealendarOLeAkоkyscrco.on(©) Hubspot.JournalPollinaService.ohp x$512Cass nuosoo vourhalroto nobeiv* Acquire exclusive polling lock to prevent aultiple instancesrivate function acquirePollingLock(): boolBullhorne CopperimermobiacteDecorateActivityDummy› @ Helpersv im HibsodAccountSyncStrategyIhctiontContactSyncStrategy› DODTO331333› E Fieldswhtaurnd© HubspotClientCredenti@HubspotDealWebhook 340© HubspotJournalApiCie341© HubspotJournalPolling: 342© HubspotWebhookSubs© JournalApiResult.php© JournalßatchSizeLimitE© JournalEventTransforma Metadataa@poortunityswncstrateoy→lmlpae natioo* Rerease the potting lockrivate function releasePollingLock(): voidl...* Force release the polling lock (for emergency situations)ublic function forceReleaseLock(): voidf...* Signat the polling service to stop gracefullyublic function requestStop(): void{...}M canicetraiteU TextRelayServiceTest~©ActivityController.php© Kernel.phpSF jiminny@localhost)•• Thu 28 May 15:18:06+0.A console (PROD] x © Service.phpds consoeleu.68fminry~045 A1 441 X 66 ACONCAT(U.10, CASE WHEN U.id = t.ouner_id THEN • (ouner) ' ELSE "* END) AS user_id,Cacaado CodoxoKick off a new project. Make changes738select x fron text_relays where created at > +2826-85-81*:select * fron activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;id IN (18688, 13934, 7160):_id = 7160 order by id desc Linit 10;select x fron activity_searches where user_id = 31054;activity_search_filters where activity_search_id IN (88882, 88902);c. Fx TexiRelay service Teste© Multi-Picklist Layout Displayis this @HubspotJournalPollingService.php#L319 ok,l")Hode swioth56mtMedeetmeooltttlcharelwhires*4 space...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85319
|
2920
|
38
|
2026-05-28T12:18:06.806990+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970686806_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","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\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"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,"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,"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,"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,"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,"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,"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,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","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":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","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":"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}]...
|
2526630547225287665
|
1137771414947247693
|
typing_pause
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85318
|
2921
|
39
|
2026-05-28T12:18:06.689033+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970686689_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.37865692,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.39029256,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39993352,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40724733,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.41589096,"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.4245346,"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.43550533,"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.44414893,"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.45279256,"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}]...
|
5244664462083781043
|
-1183062619475730196
|
typing_pause
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…...
|
85316
|
NULL
|
NULL
|
NULL
|
|
85317
|
2920
|
37
|
2026-05-28T12:17:56.858386+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970676858_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeRefactorRunToolsGi PhpStormFileEditViewNavigateCodeRefactorRunToolsGitWindowHelpec2-user@ip-10-30-129-190:~screenpipe"84‹$0(ah]A100% <78• Thu 28 May 15:17:56181ec2-user@ip-10-30-140-...₴7DOCKER#_####_#####\\###||\#/V~'881DEV (-zsh)O [EMAIL] Linux 2023 (ECS Optimized)_/m/For documentation, visit [URL_WITH_CREDENTIALS] ~]$ exitlogoutConnection to jiminny-prod-ecsi closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ applukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ vprodWarning: Permanently added 'jiminny-prod-ecs1' (ED25519) to the list of known hosts.A newer release of "Amazon Linux" is available.Version 2023.10.20260330:Version 2023.11.20260406:Version 2023.11.20260413:Version2023.11.20260427:Version2023.11.20260505:Version 2023.11.20260509:Version 2023.11.20260511:Version 2023.11.20260514:Run "/usr/bin/dnf check-release-update" for full release and version update info#_~\ ####_\ #####\\###1\#/Amazon Linux 2023 (ECS Optimized)/m/'For documentation, visit [URL_WITH_CREDENTIALS] ~]$ client_loop: send disconnect: Broken pipeukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $I...
|
NULL
|
-2845353526474952539
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeRefactorRunToolsGi PhpStormFileEditViewNavigateCodeRefactorRunToolsGitWindowHelpec2-user@ip-10-30-129-190:~screenpipe"84‹$0(ah]A100% <78• Thu 28 May 15:17:56181ec2-user@ip-10-30-140-...₴7DOCKER#_####_#####\\###||\#/V~'881DEV (-zsh)O [EMAIL] Linux 2023 (ECS Optimized)_/m/For documentation, visit [URL_WITH_CREDENTIALS] ~]$ exitlogoutConnection to jiminny-prod-ecsi closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ applukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ vprodWarning: Permanently added 'jiminny-prod-ecs1' (ED25519) to the list of known hosts.A newer release of "Amazon Linux" is available.Version 2023.10.20260330:Version 2023.11.20260406:Version 2023.11.20260413:Version2023.11.20260427:Version2023.11.20260505:Version 2023.11.20260509:Version 2023.11.20260511:Version 2023.11.20260514:Run "/usr/bin/dnf check-release-update" for full release and version update info#_~\ ####_\ #####\\###1\#/Amazon Linux 2023 (ECS Optimized)/m/'For documentation, visit [URL_WITH_CREDENTIALS] ~]$ client_loop: send disconnect: Broken pipeukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $I...
|
85315
|
NULL
|
NULL
|
NULL
|
|
85316
|
2921
|
38
|
2026-05-28T12:17:56.757485+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970676757_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lProinet vwnapers.oh©ActivityControlier.php© VoiceConsentPrefix.phpen NoticationsnObservers© AccountObserver.phpCVActvwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.pho© ProfileObserver.phg© SocialAccountObserver.php© UserObserver.pho© UserRoleObserver.phc> (l Policiesa ProvidersCn Queueu ReoositonieCn RulesvserviceDa ActivityAireoorsAvaratlealendarleonterenegRtllneonimconnenimermobiacteTm Nacorate A chivinDummym HelnarsHubspol(i) AccountSyncStrategyMAetioneD ContactSyncStrateay› DDTO› Fieldswn taueeaOLeAkоkyscrco.onc Kemelons(©) Hubspot.JournalPollinaService.ohp xpap auloloone$512cass nuosco ourhalroto noserv331333(C) HubspotClientCredenti@ HubspotDealWebhooks 340(C) Hubspot.JournalApiClie© Hubspot.JournalPolling! 348@ HubspotWebhookSubr© .JournalAoiResutt.oho© JournalBatchSizellmitEC) Journa Eventtranstorm0 Metadata@poortunityswncstrateo→lmlpae natiooprosnad SaarchChrateoU Sarnce tiraite* Acquire exclusive polling lock to prevent aultiple instances1 usageonivare functsion acausrerotsinalockio: boolSexpiresAt = now®->addSeconds(self::LOCK-T/L_SECONDS)->tolSOStringO/l Use atonic operation to set both lock and expiration dataStockData ="Locked' => true'expires at' => SexpiresAt,'acquired at' => now(->toIS0String• Extract Surround // = : }) for atonic lock acquisitionSlockAcquired = Redis::set( key: self::POLLING_LOCK,KEY, json_encode(SlockData), options: 'EX*, self:3$717Sif (SlockAcquired) "MmsoslasLorkkenewal 8eteohLog::info( message: "[HubSpot Journal Polling) Acquired polling lock', (exoNesgsexoesD):netuon tboobl Slockacausinede*Releese the nobane lock*/2 usaaesoniivate functiion releasePorsinglackot wondte73273s* Force release the polling lock (for emergency situations)3 usaoespublic function forceReleaseLock(): voidf...}gaaa* Signal the polling service to stoo gracefullepublic function requeststop): void...,tian vou can safely uninstall Hunsnell without affectina the Catonadas foe othar lanquaoas. 114 minutes aooTextRelayServiceTestA SF fiminny@localhost)•• Thu 28 May 15:17:56+0.console (PROD) x C Service.phpds consoeleu.Dojminnyv045 A1 A41 У 66 ACONCAT(u.id, CASE WHEN U.id = t.ouner_id THEN • (ouner)' ELSE "* END) AS user_1dWHERE uuid to_bin( [CREDIT_CARD]-927f-4f4da2a8185c') = uuid: # 80186192 N0Cacaado CodoxoKick off a new proinet. Make chanod.SEIFM+ EROM eo Geld data&•I0TM actvs tes a 0M &dlactvsty sdea.aWHERE activity_id = 79933459'hs_activity_type";select * fron text_relays where created_at > ^2826-05-01':select * fron activities where user id IN (7168, 18688) and created at > '2026-05-22' order by id descselect * fron users where tean_id = 1 and id IN (18688, 13934, 7169):select * fron activities where user id = 7168 order by id desc Linit 10-select x fron teansselect * fron activity searches where user_1d = 31054:activity seanch_ filters where activity_search_id TN (88882. 88992)c. Fix Texikelay service TesteC Multi-Picklist Layout Displa")Hode swioth56myWedelsteeoeltttlchareWhiroh*4 space...
|
NULL
|
-7972193424313891883
|
NULL
|
click
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lProinet vwnapers.oh©ActivityControlier.php© VoiceConsentPrefix.phpen NoticationsnObservers© AccountObserver.phpCVActvwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.pho© ProfileObserver.phg© SocialAccountObserver.php© UserObserver.pho© UserRoleObserver.phc> (l Policiesa ProvidersCn Queueu ReoositonieCn RulesvserviceDa ActivityAireoorsAvaratlealendarleonterenegRtllneonimconnenimermobiacteTm Nacorate A chivinDummym HelnarsHubspol(i) AccountSyncStrategyMAetioneD ContactSyncStrateay› DDTO› Fieldswn taueeaOLeAkоkyscrco.onc Kemelons(©) Hubspot.JournalPollinaService.ohp xpap auloloone$512cass nuosco ourhalroto noserv331333(C) HubspotClientCredenti@ HubspotDealWebhooks 340(C) Hubspot.JournalApiClie© Hubspot.JournalPolling! 348@ HubspotWebhookSubr© .JournalAoiResutt.oho© JournalBatchSizellmitEC) Journa Eventtranstorm0 Metadata@poortunityswncstrateo→lmlpae natiooprosnad SaarchChrateoU Sarnce tiraite* Acquire exclusive polling lock to prevent aultiple instances1 usageonivare functsion acausrerotsinalockio: boolSexpiresAt = now®->addSeconds(self::LOCK-T/L_SECONDS)->tolSOStringO/l Use atonic operation to set both lock and expiration dataStockData ="Locked' => true'expires at' => SexpiresAt,'acquired at' => now(->toIS0String• Extract Surround // = : }) for atonic lock acquisitionSlockAcquired = Redis::set( key: self::POLLING_LOCK,KEY, json_encode(SlockData), options: 'EX*, self:3$717Sif (SlockAcquired) "MmsoslasLorkkenewal 8eteohLog::info( message: "[HubSpot Journal Polling) Acquired polling lock', (exoNesgsexoesD):netuon tboobl Slockacausinede*Releese the nobane lock*/2 usaaesoniivate functiion releasePorsinglackot wondte73273s* Force release the polling lock (for emergency situations)3 usaoespublic function forceReleaseLock(): voidf...}gaaa* Signal the polling service to stoo gracefullepublic function requeststop): void...,tian vou can safely uninstall Hunsnell without affectina the Catonadas foe othar lanquaoas. 114 minutes aooTextRelayServiceTestA SF fiminny@localhost)•• Thu 28 May 15:17:56+0.console (PROD) x C Service.phpds consoeleu.Dojminnyv045 A1 A41 У 66 ACONCAT(u.id, CASE WHEN U.id = t.ouner_id THEN • (ouner)' ELSE "* END) AS user_1dWHERE uuid to_bin( [CREDIT_CARD]-927f-4f4da2a8185c') = uuid: # 80186192 N0Cacaado CodoxoKick off a new proinet. Make chanod.SEIFM+ EROM eo Geld data&•I0TM actvs tes a 0M &dlactvsty sdea.aWHERE activity_id = 79933459'hs_activity_type";select * fron text_relays where created_at > ^2826-05-01':select * fron activities where user id IN (7168, 18688) and created at > '2026-05-22' order by id descselect * fron users where tean_id = 1 and id IN (18688, 13934, 7169):select * fron activities where user id = 7168 order by id desc Linit 10-select x fron teansselect * fron activity searches where user_1d = 31054:activity seanch_ filters where activity_search_id TN (88882. 88992)c. Fix Texikelay service TesteC Multi-Picklist Layout Displa")Hode swioth56myWedelsteeoeltttlchareWhiroh*4 space...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85315
|
2920
|
36
|
2026-05-28T12:17:53.998788+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970673998_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","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\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"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,"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,"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,"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,"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,"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,"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,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3867889705823262686
|
-1183062619475730196
|
typing_pause
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85314
|
2921
|
37
|
2026-05-28T12:17:53.897958+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970673897_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.37865692,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.39029256,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39993352,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40724733,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.41589096,"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.4245346,"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.43550533,"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.44414893,"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.45279256,"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.4637633,"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.47473404,"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.5013298,"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.51230055,"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.7237367,"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}]...
|
490348651913652116
|
-30141114868883220
|
typing_pause
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes...
|
85313
|
NULL
|
NULL
|
NULL
|
|
85313
|
2921
|
36
|
2026-05-28T12:17:51.322679+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970671322_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.37865692,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.39029256,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39993352,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40724733,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.41589096,"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.4245346,"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.43550533,"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.44414893,"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.45279256,"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.4637633,"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.47473404,"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.5013298,"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.51230055,"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.7237367,"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":"45","depth":4,"bounds":{"left":0.6938165,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.70611703,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.71542555,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.72706115,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7390292,"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.7463431,"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":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","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"}]...
|
2526630547225287665
|
1137771414947247693
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85312
|
2920
|
35
|
2026-05-28T12:17:50.290105+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970670290_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","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}]...
|
6879326690315022146
|
-8631728821509045824
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
PhpStormFileEditViewNavigateCodeRefactorRunToolsGitWindowHelpec2-user@ip-10-30-129-190:~screenpipe"X4‹$0(ah]A100% <78• Thu 28 May 15:17:50181ec2-user@ip-10-30-140-...₴7DOCKER#_####_#####\\###||\#/V~'881DEV (-zsh)O [EMAIL] Linux 2023 (ECS Optimized)_/m/For documentation, visit [URL_WITH_CREDENTIALS] ~]$ exitlogoutConnection to jiminny-prod-ecsi closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ applukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ vprodWarning: Permanently added 'jiminny-prod-ecs1' (ED25519) to the list of known hosts.A newer release of "Amazon Linux" is available.Version 2023.10.20260330:Version 2023.11.20260406:Version 2023.11.20260413:Version2023.11.20260427:Version2023.11.20260505:Version 2023.11.20260509:Version 2023.11.20260511:Version 2023.11.20260514:Run "/usr/bin/dnf check-release-update" for full release and version update info#_~\ ####_\ #####\\###1\#/Amazon Linux 2023 (ECS Optimized)/m/'For documentation, visit [URL_WITH_CREDENTIALS] ~]$ client_loop: send disconnect: Broken pipeukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $I...
|
85311
|
NULL
|
NULL
|
NULL
|
|
85311
|
2920
|
34
|
2026-05-28T12:17:46.559275+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970666559_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","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\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"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,"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,"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,"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,"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,"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,"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,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","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":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","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":"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}]...
|
2526630547225287665
|
1137771414947247693
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85310
|
2921
|
35
|
2026-05-28T12:17:46.455990+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970666455_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.37865692,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.39029256,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39993352,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40724733,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.41589096,"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.4245346,"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.43550533,"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.44414893,"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.45279256,"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.4637633,"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.47473404,"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.5013298,"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.51230055,"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.7237367,"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}]...
|
490348651913652116
|
-30141114868883220
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes...
|
85309
|
NULL
|
NULL
|
NULL
|
|
85309
|
2921
|
34
|
2026-05-28T12:17:45.571937+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970665571_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.37865692,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.39029256,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39993352,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40724733,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.41589096,"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.4245346,"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.43550533,"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.44414893,"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.45279256,"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.4637633,"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.47473404,"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.5013298,"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.51230055,"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.7237367,"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":"45","depth":4,"bounds":{"left":0.6938165,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.70611703,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.71542555,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.72706115,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7390292,"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.7463431,"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":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","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":"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}]...
|
2526630547225287665
|
1137771414947247693
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85308
|
2921
|
33
|
2026-05-28T12:17:42.906769+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970662906_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
SluCkVIChRotonWindowHelpPwovscors#12121 on JY-2096 SluCkVIChRotonWindowHelpPwovscors#12121 on JY-20963-foc-proidetecanyice pn.ne beloers.ohtpelereobiecistraicon© VoiceConsentPrefix.phpen Noticationsv Observers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phpGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.php© UserObserver.phoUserRoleObserver.pho> (l Policiesa ProvidersCn Queue› E RepositoriosCn RulesvserviceDa ActivityAiReodrM AvatalTeallendara Bullhome CopperimermobiacteNacorateA ctivinDummym Helnarsv im Hibsod(i) AccountSyncStrategIhctiontD ContactSyncStrateay› DDTO› DFieldswn taueeaOLeAkоkyscrco.onO UserSHStoy-prpTeareolaocererestonC TextRelayServiceTest.phg(©) Hubspot.JournalPollinaService.ohp x ptp autoload.ohrcass nuosoorsournalroco noserveana lock to orevent auloole instances.3073esepolsinalocko: bool›addSeconds(self::LOCK_VTL_SECONDS) ->tolS0StringO:an theat hothtoek ann ayns.matsondtCoyns noeht> now()->toIS0StringO317318f Not eXists) for atonic lock acquisition88Renewal = tineo,100: "[HubSpot Journal Polbingl Acquired polting lock".[• => SexojinesAt.$ 3329331333PolLinglock@= woidtC UTHAAAARAAAA@ HubspotDeaMohhonie 346(C) Hubspot.JournalApiClie© Hubspot.JournalPolling! 348@ HubspotWebhookSubr© .JournalAoiResutt.oho© JournalBatchSizelimitElling lock (for emergency situations)› LeaseLock(): voidf...}C) Journa Eventtranstormervice to stop gracefullya Metadata@poortunityswncstrateo→lmlpae natioo• Stop@: void(...}M canicetraiteSamiceTesuphr©ActivityControlier.php© Team.phpe Kecneupnis=custom.logtraveuoeA console [STAGING— 78%786707708709710712-74717-218728)724733AND a.created at> DATSoGROUP BY u.id, u.email,ORDER BY sne count DESMJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM activitioSELECT * FROM actávitioSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenselect + fron plavbookeseleetron miawhonkeselect * fron playbook_cseleet ron eoeselect + fron eon Sieldselect * fron activitys7o0sLXinu co woy to.l/.HoeJiminny ... -# buescontusion-dinig@ Describe what you are looking for* €. Vasil Vasileve MessagerAdd canvasfiles.за третата точка обаче наистина ли няма подържкаDooyw00sie the neoole of fimillEontt Verhint1 Stelivan Georsie.D Vod&r MireNikolav Vanko&o James GrahamLukas Kovalky...
|
NULL
|
4682376769155574518
|
NULL
|
click
|
ocr
|
NULL
|
SluCkVIChRotonWindowHelpPwovscors#12121 on JY-2096 SluCkVIChRotonWindowHelpPwovscors#12121 on JY-20963-foc-proidetecanyice pn.ne beloers.ohtpelereobiecistraicon© VoiceConsentPrefix.phpen Noticationsv Observers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phpGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.php© UserObserver.phoUserRoleObserver.pho> (l Policiesa ProvidersCn Queue› E RepositoriosCn RulesvserviceDa ActivityAiReodrM AvatalTeallendara Bullhome CopperimermobiacteNacorateA ctivinDummym Helnarsv im Hibsod(i) AccountSyncStrategIhctiontD ContactSyncStrateay› DDTO› DFieldswn taueeaOLeAkоkyscrco.onO UserSHStoy-prpTeareolaocererestonC TextRelayServiceTest.phg(©) Hubspot.JournalPollinaService.ohp x ptp autoload.ohrcass nuosoorsournalroco noserveana lock to orevent auloole instances.3073esepolsinalocko: bool›addSeconds(self::LOCK_VTL_SECONDS) ->tolS0StringO:an theat hothtoek ann ayns.matsondtCoyns noeht> now()->toIS0StringO317318f Not eXists) for atonic lock acquisition88Renewal = tineo,100: "[HubSpot Journal Polbingl Acquired polting lock".[• => SexojinesAt.$ 3329331333PolLinglock@= woidtC UTHAAAARAAAA@ HubspotDeaMohhonie 346(C) Hubspot.JournalApiClie© Hubspot.JournalPolling! 348@ HubspotWebhookSubr© .JournalAoiResutt.oho© JournalBatchSizelimitElling lock (for emergency situations)› LeaseLock(): voidf...}C) Journa Eventtranstormervice to stop gracefullya Metadata@poortunityswncstrateo→lmlpae natioo• Stop@: void(...}M canicetraiteSamiceTesuphr©ActivityControlier.php© Team.phpe Kecneupnis=custom.logtraveuoeA console [STAGING— 78%786707708709710712-74717-218728)724733AND a.created at> DATSoGROUP BY u.id, u.email,ORDER BY sne count DESMJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM activitioSELECT * FROM actávitioSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenselect + fron plavbookeseleetron miawhonkeselect * fron playbook_cseleet ron eoeselect + fron eon Sieldselect * fron activitys7o0sLXinu co woy to.l/.HoeJiminny ... -# buescontusion-dinig@ Describe what you are looking for* €. Vasil Vasileve MessagerAdd canvasfiles.за третата точка обаче наистина ли няма подържкаDooyw00sie the neoole of fimillEontt Verhint1 Stelivan Georsie.D Vod&r MireNikolav Vanko&o James GrahamLukas Kovalky...
|
85305
|
NULL
|
NULL
|
NULL
|
|
85307
|
2920
|
33
|
2026-05-28T12:17:43.017737+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970663017_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeRefactorRunToolsGi PhpStormFileEditViewNavigateCodeRefactorRunToolsGitWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O ₴4‹$0(ah]БГ100% <78• Thu 28 May 15:17:42181ec2-user@ip-10-30-140-…₴7DOCKER#_####_#####\\###||\#/V~'881DEV (-zsh)O [EMAIL] Linux 2023 (ECS Optimized)_/m/For documentation, visit [URL_WITH_CREDENTIALS] ~]$ exitlogoutConnection to jiminny-prod-ecsi closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ applukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ vprodWarning: Permanently added 'jiminny-prod-ecs1' (ED25519) to the list of known hosts.A newer release of "Amazon Linux" is available.Version 2023.10.20260330:Version 2023.11.20260406:Version 2023.11.20260413:Version2023.11.20260427:Version2023.11.20260505:Version 2023.11.20260509:Version 2023.11.20260511:Version 2023.11.20260514:Run "/usr/bin/dnf check-release-update" for full release and version update info#_~\ ####_\ #####\\###1\#/Amazon Linux 2023 (ECS Optimized)/m/'For documentation, visit [URL_WITH_CREDENTIALS] ~]$ client_loop: send disconnect: Broken pipeukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $I...
|
NULL
|
-2124905150863850945
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeRefactorRunToolsGi PhpStormFileEditViewNavigateCodeRefactorRunToolsGitWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O ₴4‹$0(ah]БГ100% <78• Thu 28 May 15:17:42181ec2-user@ip-10-30-140-…₴7DOCKER#_####_#####\\###||\#/V~'881DEV (-zsh)O [EMAIL] Linux 2023 (ECS Optimized)_/m/For documentation, visit [URL_WITH_CREDENTIALS] ~]$ exitlogoutConnection to jiminny-prod-ecsi closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ applukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ vprodWarning: Permanently added 'jiminny-prod-ecs1' (ED25519) to the list of known hosts.A newer release of "Amazon Linux" is available.Version 2023.10.20260330:Version 2023.11.20260406:Version 2023.11.20260413:Version2023.11.20260427:Version2023.11.20260505:Version 2023.11.20260509:Version 2023.11.20260511:Version 2023.11.20260514:Run "/usr/bin/dnf check-release-update" for full release and version update info#_~\ ####_\ #####\\###1\#/Amazon Linux 2023 (ECS Optimized)/m/'For documentation, visit [URL_WITH_CREDENTIALS] ~]$ client_loop: send disconnect: Broken pipeukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $I...
|
85306
|
NULL
|
NULL
|
NULL
|
|
85264
|
2921
|
12
|
2026-05-28T12:15:59.140727+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970559140_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.37865692,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.39029256,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39993352,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40724733,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.41589096,"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.4245346,"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.43550533,"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.44414893,"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.45279256,"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.4637633,"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.47473404,"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.5013298,"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.51230055,"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.7237367,"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":"45","depth":4,"bounds":{"left":0.6938165,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.70611703,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.71542555,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.72706115,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7390292,"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.7463431,"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":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","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":"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}]...
|
2526630547225287665
|
1137771414947247693
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
12
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.own...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85263
|
2921
|
11
|
2026-05-28T12:15:57.700171+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970557700_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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}]...
|
-5599254006743683834
|
-7050965352302034496
|
typing_pause
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
rnpstomViewPVovsco.s© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phcCActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc> (l PoliciesaProvidersQueueu Reoositoniea RulesvServicesDa ActivityAiReodnimermobiacte@ DecorateActivityDummym Helnarsv m Hibsnd(i) AccountSyncStrategMAetioneMcontdSuncstrateon• D DTO› FieldsThtauendOpportunivsvncstratcov- Pac nation-ProsoecSearchStratco→- RedisService traits• Utils- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCleantono@ GiosedDealStanesSeniceclnaalise rccarica onoCoocWindowwnapers.ohOLenkоkyseiwico.onMWiaKorCass wochexe* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)ScacheKey = Sthis->getRateLimitCacheKeyOScacheotxozresac = keozst.oet scachekey)*if ds string(ScachedExpiresAt)) 8& is numeric(ScachSremaining = naxl vaano scachedhrow new kaceLinzcexcepe.onsrenaining,neruonSantwaittos} catch (Throvable Se)!i4 (Sthiso>isHubsootRateLinit(Se))metnwAttenThisorrarse?erry.trer teNx: oniy the Sinet doh to receive a 420Subseauent 0s in the sane burct leaveIl window is not reset by every concurrentRediceeset(Scachekey. (strino) (eine0) 4 So 319Sthie-slog-smaroino(*[Huhsnotl Recesved 420 32:iteanSdles Sthte-sconfioestean sa 32:'configid'= Sthís-scontsocsoettala 323as Scetryatter= Se->getMessageOthrow new RateLinitException message: "Hubsr S2Throw Senprivate function getRateLinitCachekevO: strinol...;TO0% L7inu co moy to.loro+0.ActivityControlker.thp=custom.logaraveuesA SF fiminny@localhost)HS Jocal (jminny@localhost)okeinuironeA console [STAGINGC) Client.php X php autoload.phgDa00Eind in Files match inathiee maskondSockacquired = Redis"ser seheporuing lock key ison encodcislockoala eXSeRLOCK TTL SECONDS,INXYx5 Cc wIn Project Module Directory Scoped = Redis::set(self:POLLING_LOCK,KEY, Ison_encode(SlockData), "EX, self:LOCK_TTL_SECONDS, HubspotJourna Po ngService.php 319d console (PROD) x C Service.phpds consoeleu.Do jminny v0458185Y0 AEND AS user id1896856248 = UU1d: # 79953459 Y2=4da2a8185c') = uuid: # 80186192 NỎHubspot.JournalPollingService.ohp app/Services/Crm/Hubsoot/JolUse aromic opcration to ser both lock and exotracion daraSlockData =1•ockedexorresn ss seyotres.tacautredar s nomeetoisienaon// Use SETNX (SET if Not eXists) for atonic lock acquisitionSlockAcquired = Redis::set( key: self::POLLING_LOCK_KEY, json_encode(SlockData), optiom"Ex" sole-atnoki sea+SlockAcous cedh$this->lastLockRenewal = tineO:Log: : info( message:'[HubSpot Journal Polling] Acquired polling Lock', daynsnes ati = Seyasresat.ID:return (bool) SlockAcquired:* Release the polZing lockprivate function releasePolZinglockO: voideoen rosutsiin now neeoeninanene oinCacaado CodoxoKick of a new proisct. Make chunoe> *2026-05-22' order by id desc3824889822c. Fix Texikelay service TesteC Multi-Picklist Layout Displatent.ohoat.9om1a,"Pode swioth• oNtwndeurTasme oeii.s...
|
85261
|
NULL
|
NULL
|
NULL
|
|
85262
|
2920
|
10
|
2026-05-28T12:15:57.807255+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970557807_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotJournalPollingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.em...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Journal;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\Webhook\\WebhookEventProcessor;\n\nclass HubspotJournalPollingService\n{\n private const string OFFSET_CACHE_KEY = 'hubspot_journal_offset';\n private const string POLLING_LOCK_KEY = 'hubspot_journal_polling_lock';\n private const string STOP_FLAG_KEY = 'hubspot_journal_stop_flag';\n private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds\n private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds\n private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data\n private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration\n private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep\n private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping\n private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit\n private const int MAX_BACKOFF_SECONDS = 300;\n private const int MAX_OFFSET_RETRIES = 3;\n private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes\n private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;\n private const int LOG_INTERVAL_CYCLES = 10;\n\n private bool $shouldStop = false;\n private int $consecutiveEmptyResults = 0;\n private array $offsetRetryCount = [];\n\n private int $cycleCount = 0;\n private int $startTime = 0;\n private int $lastLockRenewal = 0;\n\n private float $totalApiTime = 0;\n private float $totalDownloadTime = 0;\n private float $totalTransformTime = 0;\n private float $totalProcessTime = 0;\n\n private int $totalJournalFilesDownloaded = 0;\n private int $totalEventsProcessed = 0;\n private int $emptyJournalFiles = 0;\n private int $otherPortalSkipped = 0;\n\n public function __construct(\n private HubspotJournalApiClient $apiClient,\n private JournalEventTransformer $transformer,\n private WebhookEventProcessor $eventProcessor\n ) {\n }\n\n /**\n * Start continuous polling of the HubSpot journal\n */\n public function startPolling(): void\n {\n $this->startTime = time();\n $this->cycleCount = 0;\n $this->logPollingStart();\n\n if (! $this->acquirePollingLock()) {\n Log::warning('[HubSpot Journal Polling] Another polling process is already running');\n\n return;\n }\n\n try {\n while ($this->shouldContinuePolling()) {\n $this->cycleCount++;\n $this->executePollingCycle();\n }\n } catch (\\Throwable $e) {\n Log::error('[HubSpot Journal Polling] Error while polling', [\n 'error' => $e->getMessage(),\n 'trace' => $e->getTraceAsString(),\n ]);\n } finally {\n $this->cleanupPollingSession();\n }\n }\n\n /**\n * Perform a single polling cycle\n * Returns true if work was found and processed, false if no work available, null if entry was skipped\n */\n public function pollOnce(): ?bool\n {\n $currentOffset = $this->getCurrentOffset();\n\n $apiStart = microtime(true);\n $apiResult = $currentOffset === null\n ? $this->apiClient->getLatestJournalEntry()\n : $this->apiClient->getNextJournalEntry($currentOffset);\n $this->totalApiTime += (microtime(true) - $apiStart) * 1000;\n\n if (! $apiResult->success) {\n return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');\n }\n\n if ($apiResult->data === null) {\n Log::info('[HubSpot Journal Polling] No data');\n\n return false;\n }\n\n $offset = $apiResult->data['currentOffset'];\n $s3Url = $apiResult->data['url'];\n $retryCount = $this->offsetRetryCount[$offset] ?? 0;\n\n $downloadStart = microtime(true);\n $downloadResult = $this->apiClient->downloadJournalFile($s3Url);\n $this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;\n\n if (! $downloadResult->success) {\n return $this->handleDownloadResult($downloadResult, $offset, $retryCount);\n }\n\n $this->totalJournalFilesDownloaded++;\n $journalData = $downloadResult->data;\n\n $transformStart = microtime(true);\n\n try {\n $transformedEvents = $this->transformer->transformJournalEvents($journalData);\n } catch (\\Exception $e) {\n return $this->handleTransformationFailure($offset, $retryCount, $e);\n }\n $this->totalTransformTime += (microtime(true) - $transformStart) * 1000;\n unset($journalData);\n\n $processStart = microtime(true);\n $eventsCount = count($transformedEvents);\n $this->totalEventsProcessed += $eventsCount;\n\n if ($eventsCount === 0) {\n $this->emptyJournalFiles++;\n } else {\n $this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');\n }\n $this->totalProcessTime += (microtime(true) - $processStart) * 1000;\n\n $this->updateOffset($offset);\n unset($transformedEvents);\n\n $this->logProgressIfNeeded();\n\n return $eventsCount > 0;\n }\n\n private function logProgressIfNeeded(): void\n {\n if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {\n return;\n }\n\n $cycles = $this->cycleCount;\n $avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;\n\n Log::info('[HubSpot Journal Polling] Progress', [\n 'cycles' => $cycles,\n 'files' => $this->totalJournalFilesDownloaded,\n 'events' => $this->totalEventsProcessed,\n 'empty_files' => $this->emptyJournalFiles,\n 'avg_ms' => round($avgTotal, 1),\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n ]);\n }\n\n /**\n * Get the current polling offset from Redis\n */\n private function getCurrentOffset(bool $cacheOnly = false): ?string\n {\n $offset = Redis::get(self::OFFSET_CACHE_KEY);\n\n if (! $offset && ! $cacheOnly) {\n $offset = $this->getDbOffset();\n }\n\n return $offset ? (string) $offset : null;\n }\n\n /**\n * Get the last stored offset from the database\n */\n public function getDbOffset(): ?string\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return null;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');\n\n Log::info('[HubSpot Journal Polling] Getting offset from database', [\n 'offset' => $offset,\n 'jiminny_team_id' => $jiminnyTeam->getId(),\n ]);\n\n return $offset;\n }\n\n /**\n * Reset the database offset\n */\n public function resetDbOffset(): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');\n\n Log::info('[HubSpot Journal Polling] Database offset reset');\n }\n\n private function getJiminnyTeam(): ?Team\n {\n $teamRepository = app(TeamRepository::class);\n\n return $teamRepository->getTeamBySlug('jiminny');\n }\n\n private function updateDbOffset(string $offset): void\n {\n $jiminnyTeam = $this->getJiminnyTeam();\n\n if (! $jiminnyTeam instanceof Team) {\n Log::error('[HubSpot Journal Polling] Jiminny team not found');\n\n return;\n }\n\n $crmRepository = app(CrmConfigurationRepository::class);\n\n $crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);\n }\n\n /**\n * Update the polling offset\n */\n private function updateOffset(string $offset): void\n {\n Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL\n $this->clearOffsetRetryCount($offset);\n }\n\n /**\n * Reset the polling offset\n */\n public function resetOffset(): void\n {\n Redis::del(self::OFFSET_CACHE_KEY);\n Log::info('[HubSpot Journal Polling] Reset polling offset');\n }\n\n /**\n * Set a specific offset (for manual override or recovery)\n */\n public function setOffset(string $offset): void\n {\n $this->updateOffset($offset);\n\n Log::warning('[HubSpot Journal Polling] Offset manually set', [\n 'offset' => $offset,\n ]);\n }\n\n /**\n * Get current polling status\n */\n public function getPollingStatus(): array\n {\n $lockDataJson = Redis::get(self::POLLING_LOCK_KEY);\n $lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;\n\n return [\n 'is_locked' => $lockData !== null,\n 'current_offset' => $this->getCurrentOffset(),\n 'lock_expires_at' => $lockData['expires_at'] ?? null,\n 'lock_acquired_at' => $lockData['acquired_at'] ?? null,\n ];\n }\n\n /**\n * Acquire exclusive polling lock to prevent multiple instances\n */\n private function acquirePollingLock(): bool\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n // Use atomic operation to set both lock and expiration data\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n ];\n\n // Use SETNX (SET if Not eXists) for atomic lock acquisition\n $lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');\n\n if ($lockAcquired) {\n $this->lastLockRenewal = time();\n Log::info('[HubSpot Journal Polling] Acquired polling lock', [\n 'expires_at' => $expiresAt,\n ]);\n }\n\n return (bool) $lockAcquired;\n }\n\n /**\n * Release the polling lock\n */\n private function releasePollingLock(): void\n {\n Redis::del(self::POLLING_LOCK_KEY);\n Log::info('[HubSpot Journal Polling] Released polling lock');\n }\n\n /**\n * Force release the polling lock (for emergency situations)\n */\n public function forceReleaseLock(): void\n {\n $this->releasePollingLock();\n Log::warning('[HubSpot Journal Polling] Force released polling lock');\n }\n\n /**\n * Signal the polling service to stop gracefully\n */\n public function requestStop(): void\n {\n Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL\n $this->shouldStop = true;\n Log::info('[HubSpot Journal Polling] Stop requested');\n }\n\n /**\n * Check if polling should stop\n */\n private function shouldStop(): bool\n {\n // Check local flag first (faster)\n if ($this->shouldStop) {\n return true;\n }\n\n // Check Redis flag (for external stop requests)\n if (Redis::exists(self::STOP_FLAG_KEY)) {\n $this->shouldStop = true;\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Clear the stop flag and reset state to allow service to start\n */\n public function clearStopFlag(): void\n {\n Redis::del(self::STOP_FLAG_KEY);\n $this->shouldStop = false;\n $this->resetPollingState();\n\n Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');\n }\n\n /**\n * Reset polling state without clearing stop flag\n */\n private function resetPollingState(): void\n {\n $this->consecutiveEmptyResults = 0;\n $this->cycleCount = 0;\n $this->startTime = 0;\n $this->totalApiTime = 0;\n $this->totalDownloadTime = 0;\n $this->totalTransformTime = 0;\n $this->totalProcessTime = 0;\n $this->totalJournalFilesDownloaded = 0;\n $this->totalEventsProcessed = 0;\n $this->emptyJournalFiles = 0;\n $this->otherPortalSkipped = 0;\n }\n\n public function hasStopFlag(): bool\n {\n return (bool) Redis::exists(self::STOP_FLAG_KEY);\n }\n\n /**\n * Calculate adaptive sleep duration based on consecutive empty results\n */\n private function calculateAdaptiveSleep(): int\n {\n if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {\n return self::BASE_SLEEP_SECONDS; // 5 second\n }\n\n // Progressive backoff: 15s -> 30s -> 60s -> 120s (max)\n $delay = 3 * self::BASE_SLEEP_SECONDS;\n $sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));\n\n return min($sleepDuration, self::MAX_SLEEP_SECONDS);\n }\n\n private function logPollingStart(): void\n {\n Log::info('[HubSpot Journal Polling] Service starting', [\n 'memory_limit' => ini_get('memory_limit'),\n 'max_execution_time' => ini_get('max_execution_time'),\n 'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Log polling service end information\n */\n private function logPollingEnd(): void\n {\n $runtime = time() - $this->startTime;\n $cycles = max($this->cycleCount, 1);\n\n Log::info('[HubSpot Journal Polling] Service ending', [\n 'runtime_seconds' => $runtime,\n 'total_cycles' => $this->cycleCount,\n 'files_downloaded' => $this->totalJournalFilesDownloaded,\n 'empty_files' => $this->emptyJournalFiles,\n 'other_portal_skipped' => $this->otherPortalSkipped,\n 'total_events' => $this->totalEventsProcessed,\n 'events_per_file' => $this->totalJournalFilesDownloaded > 0\n ? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)\n : 0,\n 'avg_api_ms' => round($this->totalApiTime / $cycles, 1),\n 'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),\n 'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),\n 'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),\n 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),\n ]);\n }\n\n /**\n * Improved stop condition check with clear reasoning\n */\n private function shouldContinuePolling(): bool\n {\n // Check explicit stop request first (fastest check)\n if ($this->shouldStop()) {\n Log::info('[HubSpot Journal Polling] Stop requested, ending polling');\n\n return false;\n }\n\n if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {\n Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [\n 'empty_results' => $this->consecutiveEmptyResults,\n 'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,\n ]);\n\n return false;\n }\n\n // Check runtime limit (4 minutes)\n $runtime = time() - $this->startTime;\n if ($runtime >= self::MAX_RUNTIME_SECONDS) {\n Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [\n 'runtime_seconds' => $runtime,\n 'runtime_minutes' => round($runtime / 60, 1),\n 'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,\n ]);\n\n return false;\n }\n\n // Check memory limits (prevent OOM)\n $currentMemory = memory_get_usage(true);\n $memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));\n\n if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {\n Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [\n 'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),\n 'memory_limit' => ini_get('memory_limit'),\n 'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),\n ]);\n\n return false;\n }\n\n // Check for excessive cycles (prevent runaway processes)\n if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {\n Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [\n 'cycles' => $this->cycleCount,\n 'max_cycles' => self::MAX_CYCLES_DEFAULT,\n ]);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Handle when work is found - reset counters and continue immediately\n */\n private function handleWorkFound(): void\n {\n $this->consecutiveEmptyResults = 0;\n }\n\n /**\n * Handle when no work is found - implement adaptive sleep\n */\n private function handleNoWorkFound(): void\n {\n $this->consecutiveEmptyResults++;\n $sleepDuration = $this->calculateAdaptiveSleep();\n\n Log::debug('[HubSpot Journal Polling] No work found, sleeping', [\n 'consecutive_empty' => $this->consecutiveEmptyResults,\n 'sleep_seconds' => $sleepDuration,\n 'cycle' => $this->cycleCount,\n ]);\n\n $this->responsiveSleep($sleepDuration);\n }\n\n /**\n * Sleep with responsive stop checking\n */\n private function responsiveSleep(int $seconds): void\n {\n for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {\n sleep(1);\n }\n }\n\n /**\n * Parse memory limit string to bytes\n */\n private function parseMemoryLimit(string $memoryLimit): int\n {\n if ($memoryLimit === '-1') {\n return 0; // Unlimited\n }\n\n $unit = strtolower(substr($memoryLimit, -1));\n $value = (int) substr($memoryLimit, 0, -1);\n\n return match ($unit) {\n 'g' => $value * 1024 * 1024 * 1024,\n 'm' => $value * 1024 * 1024,\n 'k' => $value * 1024,\n default => (int) $memoryLimit,\n };\n }\n\n private function executePollingCycle(): void\n {\n $this->renewLockIfNeeded();\n\n $result = $this->pollOnce();\n\n if ($result === true) {\n $this->handleWorkFound();\n } elseif ($result === false) {\n $this->handleNoWorkFound();\n }\n }\n\n private function renewLockIfNeeded(): void\n {\n $now = time();\n if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {\n $this->renewPollingLock();\n $this->lastLockRenewal = $now;\n }\n }\n\n private function renewPollingLock(): void\n {\n $expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();\n\n $lockData = [\n 'locked' => true,\n 'expires_at' => $expiresAt,\n 'acquired_at' => now()->toISOString(),\n 'renewed_at' => now()->toISOString(),\n 'cycle' => $this->cycleCount,\n ];\n\n Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));\n\n Log::debug('[HubSpot Journal Polling] Lock renewed', [\n 'expires_at' => $expiresAt,\n 'cycle' => $this->cycleCount,\n ]);\n }\n\n private function handleApiResult(JournalApiResult $result, string $context): bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_AUTH) {\n Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [\n 'context' => $context,\n ]);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {\n $waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);\n Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [\n 'wait_seconds' => $waitSeconds,\n ]);\n $this->responsiveSleep($waitSeconds);\n\n return false;\n }\n\n if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {\n Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [\n 'expired_offset' => $context,\n ]);\n $this->resetOffset();\n $this->resetDbOffset();\n\n return false;\n }\n\n if ($result->isPermanentFailure()) {\n Log::error('[HubSpot Journal Polling] Permanent API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->shouldStop = true;\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable API failure', [\n 'context' => $context,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Clean up polling session resources\n */\n private function cleanupPollingSession(): void\n {\n $this->logPollingEnd();\n $this->storeOffsetInDB();\n $this->releasePollingLock();\n $this->apiClient->clearPortalCache();\n $this->resetPollingState();\n }\n\n private function storeOffsetInDB(): void\n {\n // Save current offset to database before cleanup\n $currentOffset = $this->getCurrentOffset(cacheOnly: true);\n if ($currentOffset) {\n $this->updateDbOffset($currentOffset);\n\n Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [\n 'offset' => $currentOffset,\n ]);\n }\n }\n\n private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool\n {\n if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {\n $this->otherPortalSkipped++;\n $this->updateOffset($offset);\n\n return null;\n }\n\n if ($result->isPermanentFailure()) {\n Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'error' => $result->errorMessage,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [\n 'offset' => $offset,\n 'portal_id' => $result->portalId,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n ]);\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [\n 'offset' => $offset,\n 'failure_type' => $result->failureType,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n if ($result->retryAfterSeconds) {\n sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));\n }\n\n return false;\n }\n\n /**\n * Handle transformation failure with retry logic\n */\n private function handleTransformationFailure(string $offset, int $retryCount, \\Exception $exception): bool\n {\n $retryCount++;\n\n if ($retryCount >= self::MAX_OFFSET_RETRIES) {\n Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'error' => $exception->getMessage(),\n 'trace' => $exception->getTraceAsString(),\n ]);\n\n // Skip this offset to prevent infinite loop with corrupted data\n $this->updateOffset($offset);\n $this->clearOffsetRetryCount($offset);\n\n return false;\n }\n\n Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [\n 'offset' => $offset,\n 'retry_count' => $retryCount,\n 'max_retries' => self::MAX_OFFSET_RETRIES,\n 'error' => $exception->getMessage(),\n ]);\n\n $this->offsetRetryCount[$offset] = $retryCount;\n\n return false;\n }\n\n /**\n * Clear retry count for an offset after successful processing\n */\n private function clearOffsetRetryCount(string $offset): void\n {\n unset($this->offsetRetryCount[$offset]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"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,"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,"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,"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,"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,"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,"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,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","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":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","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":"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}]...
|
-4379827447387526057
|
1137771414947247693
|
typing_pause
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Journal;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Crm\Hubspot\Webhook\WebhookEventProcessor;
class HubspotJournalPollingService
{
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const string [ENV_SECRET];
private const int LOCK_TTL_SECONDS = 120; // 2 minutes - renewed every 30 seconds
private const int LOCK_RENEWAL_INTERVAL = 30; // Renew lock every 30 seconds
private const int BASE_SLEEP_SECONDS = 5; // Base sleep when no data
private const int MAX_SLEEP_SECONDS = 120; // Maximum sleep duration
private const int EMPTY_THRESHOLD_FOR_BACKOFF = 3; // Empty results before increasing sleep
private const int MAX_CYCLES_DEFAULT = 3000; // Default maximum cycles before stopping
private const int MEMORY_CRITICAL_THRESHOLD = 90; // 90% of memory limit
private const int MAX_BACKOFF_SECONDS = 300;
private const int MAX_OFFSET_RETRIES = 3;
private const int MAX_RUNTIME_SECONDS = 240; // 4 minutes
private const int MAX_EMPTY_RESULTS_BEFORE_EXIT = 5;
private const int LOG_INTERVAL_CYCLES = 10;
private bool $shouldStop = false;
private int $consecutiveEmptyResults = 0;
private array $offsetRetryCount = [];
private int $cycleCount = 0;
private int $startTime = 0;
private int $lastLockRenewal = 0;
private float $totalApiTime = 0;
private float $totalDownloadTime = 0;
private float $totalTransformTime = 0;
private float $totalProcessTime = 0;
private int $totalJournalFilesDownloaded = 0;
private int $totalEventsProcessed = 0;
private int $emptyJournalFiles = 0;
private int $otherPortalSkipped = 0;
public function __construct(
private HubspotJournalApiClient $apiClient,
private JournalEventTransformer $transformer,
private WebhookEventProcessor $eventProcessor
) {
}
/**
* Start continuous polling of the HubSpot journal
*/
public function startPolling(): void
{
$this->startTime = time();
$this->cycleCount = 0;
$this->logPollingStart();
if (! $this->acquirePollingLock()) {
Log::warning('[HubSpot Journal Polling] Another polling process is already running');
return;
}
try {
while ($this->shouldContinuePolling()) {
$this->cycleCount++;
$this->executePollingCycle();
}
} catch (\Throwable $e) {
Log::error('[HubSpot Journal Polling] Error while polling', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} finally {
$this->cleanupPollingSession();
}
}
/**
* Perform a single polling cycle
* Returns true if work was found and processed, false if no work available, null if entry was skipped
*/
public function pollOnce(): ?bool
{
$currentOffset = $this->getCurrentOffset();
$apiStart = microtime(true);
$apiResult = $currentOffset === null
? $this->apiClient->getLatestJournalEntry()
: $this->apiClient->getNextJournalEntry($currentOffset);
$this->totalApiTime += (microtime(true) - $apiStart) * 1000;
if (! $apiResult->success) {
return $this->handleApiResult($apiResult, $currentOffset ?? 'latest');
}
if ($apiResult->data === null) {
Log::info('[HubSpot Journal Polling] No data');
return false;
}
$offset = $apiResult->data['currentOffset'];
$s3Url = $apiResult->data['url'];
$retryCount = $this->offsetRetryCount[$offset] ?? 0;
$downloadStart = microtime(true);
$downloadResult = $this->apiClient->downloadJournalFile($s3Url);
$this->totalDownloadTime += (microtime(true) - $downloadStart) * 1000;
if (! $downloadResult->success) {
return $this->handleDownloadResult($downloadResult, $offset, $retryCount);
}
$this->totalJournalFilesDownloaded++;
$journalData = $downloadResult->data;
$transformStart = microtime(true);
try {
$transformedEvents = $this->transformer->transformJournalEvents($journalData);
} catch (\Exception $e) {
return $this->handleTransformationFailure($offset, $retryCount, $e);
}
$this->totalTransformTime += (microtime(true) - $transformStart) * 1000;
unset($journalData);
$processStart = microtime(true);
$eventsCount = count($transformedEvents);
$this->totalEventsProcessed += $eventsCount;
if ($eventsCount === 0) {
$this->emptyJournalFiles++;
} else {
$this->eventProcessor->processEvents($transformedEvents, '[HubSpot Journal Polling]');
}
$this->totalProcessTime += (microtime(true) - $processStart) * 1000;
$this->updateOffset($offset);
unset($transformedEvents);
$this->logProgressIfNeeded();
return $eventsCount > 0;
}
private function logProgressIfNeeded(): void
{
if ($this->cycleCount === 0 || $this->cycleCount % self::LOG_INTERVAL_CYCLES !== 0) {
return;
}
$cycles = $this->cycleCount;
$avgTotal = ($this->totalApiTime + $this->totalDownloadTime + $this->totalTransformTime + $this->totalProcessTime) / $cycles;
Log::info('[HubSpot Journal Polling] Progress', [
'cycles' => $cycles,
'files' => $this->totalJournalFilesDownloaded,
'events' => $this->totalEventsProcessed,
'empty_files' => $this->emptyJournalFiles,
'avg_ms' => round($avgTotal, 1),
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
]);
}
/**
* Get the current polling offset from Redis
*/
private function getCurrentOffset(bool $cacheOnly = false): ?string
{
$offset = Redis::get(self::OFFSET_CACHE_KEY);
if (! $offset && ! $cacheOnly) {
$offset = $this->getDbOffset();
}
return $offset ? (string) $offset : null;
}
/**
* Get the last stored offset from the database
*/
public function getDbOffset(): ?string
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return null;
}
$crmRepository = app(CrmConfigurationRepository::class);
$offset = $crmRepository->getSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset');
Log::info('[HubSpot Journal Polling] Getting offset from database', [
'offset' => $offset,
'jiminny_team_id' => $jiminnyTeam->getId(),
]);
return $offset;
}
/**
* Reset the database offset
*/
public function resetDbOffset(): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', '');
Log::info('[HubSpot Journal Polling] Database offset reset');
}
private function getJiminnyTeam(): ?Team
{
$teamRepository = app(TeamRepository::class);
return $teamRepository->getTeamBySlug('jiminny');
}
private function updateDbOffset(string $offset): void
{
$jiminnyTeam = $this->getJiminnyTeam();
if (! $jiminnyTeam instanceof Team) {
Log::error('[HubSpot Journal Polling] Jiminny team not found');
return;
}
$crmRepository = app(CrmConfigurationRepository::class);
$crmRepository->updateSetting($jiminnyTeam->getCrmConfiguration(), 'hubspot_journal_offset', $offset);
}
/**
* Update the polling offset
*/
private function updateOffset(string $offset): void
{
Redis::setex(self::OFFSET_CACHE_KEY, 259200, $offset); // 3 days TTL
$this->clearOffsetRetryCount($offset);
}
/**
* Reset the polling offset
*/
public function resetOffset(): void
{
Redis::del(self::OFFSET_CACHE_KEY);
Log::info('[HubSpot Journal Polling] Reset polling offset');
}
/**
* Set a specific offset (for manual override or recovery)
*/
public function setOffset(string $offset): void
{
$this->updateOffset($offset);
Log::warning('[HubSpot Journal Polling] Offset manually set', [
'offset' => $offset,
]);
}
/**
* Get current polling status
*/
public function getPollingStatus(): array
{
$lockDataJson = Redis::get(self::POLLING_LOCK_KEY);
$lockData = $lockDataJson ? json_decode($lockDataJson, true) : null;
return [
'is_locked' => $lockData !== null,
'current_offset' => $this->getCurrentOffset(),
'lock_expires_at' => $lockData['expires_at'] ?? null,
'lock_acquired_at' => $lockData['acquired_at'] ?? null,
];
}
/**
* Acquire exclusive polling lock to prevent multiple instances
*/
private function acquirePollingLock(): bool
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
// Use atomic operation to set both lock and expiration data
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
];
// Use SETNX (SET if Not eXists) for atomic lock acquisition
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
if ($lockAcquired) {
$this->lastLockRenewal = time();
Log::info('[HubSpot Journal Polling] Acquired polling lock', [
'expires_at' => $expiresAt,
]);
}
return (bool) $lockAcquired;
}
/**
* Release the polling lock
*/
private function releasePollingLock(): void
{
Redis::del(self::POLLING_LOCK_KEY);
Log::info('[HubSpot Journal Polling] Released polling lock');
}
/**
* Force release the polling lock (for emergency situations)
*/
public function forceReleaseLock(): void
{
$this->releasePollingLock();
Log::warning('[HubSpot Journal Polling] Force released polling lock');
}
/**
* Signal the polling service to stop gracefully
*/
public function requestStop(): void
{
Redis::setex(self::STOP_FLAG_KEY, 300, '1'); // 5 minutes TTL
$this->shouldStop = true;
Log::info('[HubSpot Journal Polling] Stop requested');
}
/**
* Check if polling should stop
*/
private function shouldStop(): bool
{
// Check local flag first (faster)
if ($this->shouldStop) {
return true;
}
// Check Redis flag (for external stop requests)
if (Redis::exists(self::STOP_FLAG_KEY)) {
$this->shouldStop = true;
return true;
}
return false;
}
/**
* Clear the stop flag and reset state to allow service to start
*/
public function clearStopFlag(): void
{
Redis::del(self::STOP_FLAG_KEY);
$this->shouldStop = false;
$this->resetPollingState();
Log::info('[HubSpot Journal Polling] Stop flag cleared and state reset');
}
/**
* Reset polling state without clearing stop flag
*/
private function resetPollingState(): void
{
$this->consecutiveEmptyResults = 0;
$this->cycleCount = 0;
$this->startTime = 0;
$this->totalApiTime = 0;
$this->totalDownloadTime = 0;
$this->totalTransformTime = 0;
$this->totalProcessTime = 0;
$this->totalJournalFilesDownloaded = 0;
$this->totalEventsProcessed = 0;
$this->emptyJournalFiles = 0;
$this->otherPortalSkipped = 0;
}
public function hasStopFlag(): bool
{
return (bool) Redis::exists(self::STOP_FLAG_KEY);
}
/**
* Calculate adaptive sleep duration based on consecutive empty results
*/
private function calculateAdaptiveSleep(): int
{
if ($this->consecutiveEmptyResults < self::EMPTY_THRESHOLD_FOR_BACKOFF) {
return self::BASE_SLEEP_SECONDS; // 5 second
}
// Progressive backoff: 15s -> 30s -> 60s -> 120s (max)
$delay = 3 * self::BASE_SLEEP_SECONDS;
$sleepDuration = $delay * (2 ** ($this->consecutiveEmptyResults - self::EMPTY_THRESHOLD_FOR_BACKOFF));
return min($sleepDuration, self::MAX_SLEEP_SECONDS);
}
private function logPollingStart(): void
{
Log::info('[HubSpot Journal Polling] Service starting', [
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'initial_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Log polling service end information
*/
private function logPollingEnd(): void
{
$runtime = time() - $this->startTime;
$cycles = max($this->cycleCount, 1);
Log::info('[HubSpot Journal Polling] Service ending', [
'runtime_seconds' => $runtime,
'total_cycles' => $this->cycleCount,
'files_downloaded' => $this->totalJournalFilesDownloaded,
'empty_files' => $this->emptyJournalFiles,
'other_portal_skipped' => $this->otherPortalSkipped,
'total_events' => $this->totalEventsProcessed,
'events_per_file' => $this->totalJournalFilesDownloaded > 0
? round($this->totalEventsProcessed / $this->totalJournalFilesDownloaded, 1)
: 0,
'avg_api_ms' => round($this->totalApiTime / $cycles, 1),
'avg_download_ms' => round($this->totalDownloadTime / $cycles, 1),
'avg_transform_ms' => round($this->totalTransformTime / $cycles, 1),
'avg_process_ms' => round($this->totalProcessTime / $cycles, 1),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
/**
* Improved stop condition check with clear reasoning
*/
private function shouldContinuePolling(): bool
{
// Check explicit stop request first (fastest check)
if ($this->shouldStop()) {
Log::info('[HubSpot Journal Polling] Stop requested, ending polling');
return false;
}
if ($this->consecutiveEmptyResults >= self::MAX_EMPTY_RESULTS_BEFORE_EXIT) {
Log::warning('[HubSpot Journal Polling] Maximum empty results reached, stopping', [
'empty_results' => $this->consecutiveEmptyResults,
'max_empty_results' => self::MAX_EMPTY_RESULTS_BEFORE_EXIT,
]);
return false;
}
// Check runtime limit (4 minutes)
$runtime = time() - $this->startTime;
if ($runtime >= self::MAX_RUNTIME_SECONDS) {
Log::warning('[HubSpot Journal Polling] Maximum runtime reached, stopping', [
'runtime_seconds' => $runtime,
'runtime_minutes' => round($runtime / 60, 1),
'max_runtime_seconds' => self::MAX_RUNTIME_SECONDS,
]);
return false;
}
// Check memory limits (prevent OOM)
$currentMemory = memory_get_usage(true);
$memoryLimitBytes = $this->parseMemoryLimit(ini_get('memory_limit'));
if ($memoryLimitBytes > 0 && $currentMemory > ($memoryLimitBytes * self::MEMORY_CRITICAL_THRESHOLD / 100)) {
Log::warning('[HubSpot Journal Polling] Memory limit approaching, stopping', [
'current_memory_mb' => round($currentMemory / 1024 / 1024, 2),
'memory_limit' => ini_get('memory_limit'),
'usage_percent' => round(($currentMemory / $memoryLimitBytes) * 100, 1),
]);
return false;
}
// Check for excessive cycles (prevent runaway processes)
if ($this->cycleCount > self::MAX_CYCLES_DEFAULT) {
Log::warning('[HubSpot Journal Polling] Maximum cycles reached, stopping', [
'cycles' => $this->cycleCount,
'max_cycles' => self::MAX_CYCLES_DEFAULT,
]);
return false;
}
return true;
}
/**
* Handle when work is found - reset counters and continue immediately
*/
private function handleWorkFound(): void
{
$this->consecutiveEmptyResults = 0;
}
/**
* Handle when no work is found - implement adaptive sleep
*/
private function handleNoWorkFound(): void
{
$this->consecutiveEmptyResults++;
$sleepDuration = $this->calculateAdaptiveSleep();
Log::debug('[HubSpot Journal Polling] No work found, sleeping', [
'consecutive_empty' => $this->consecutiveEmptyResults,
'sleep_seconds' => $sleepDuration,
'cycle' => $this->cycleCount,
]);
$this->responsiveSleep($sleepDuration);
}
/**
* Sleep with responsive stop checking
*/
private function responsiveSleep(int $seconds): void
{
for ($i = 0; $i < $seconds && $this->shouldContinuePolling(); $i++) {
sleep(1);
}
}
/**
* Parse memory limit string to bytes
*/
private function parseMemoryLimit(string $memoryLimit): int
{
if ($memoryLimit === '-1') {
return 0; // Unlimited
}
$unit = strtolower(substr($memoryLimit, -1));
$value = (int) substr($memoryLimit, 0, -1);
return match ($unit) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => (int) $memoryLimit,
};
}
private function executePollingCycle(): void
{
$this->renewLockIfNeeded();
$result = $this->pollOnce();
if ($result === true) {
$this->handleWorkFound();
} elseif ($result === false) {
$this->handleNoWorkFound();
}
}
private function renewLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastLockRenewal >= self::LOCK_RENEWAL_INTERVAL) {
$this->renewPollingLock();
$this->lastLockRenewal = $now;
}
}
private function renewPollingLock(): void
{
$expiresAt = now()->addSeconds(self::LOCK_TTL_SECONDS)->toISOString();
$lockData = [
'locked' => true,
'expires_at' => $expiresAt,
'acquired_at' => now()->toISOString(),
'renewed_at' => now()->toISOString(),
'cycle' => $this->cycleCount,
];
Redis::setex(self::POLLING_LOCK_KEY, self::LOCK_TTL_SECONDS, json_encode($lockData));
Log::debug('[HubSpot Journal Polling] Lock renewed', [
'expires_at' => $expiresAt,
'cycle' => $this->cycleCount,
]);
}
private function handleApiResult(JournalApiResult $result, string $context): bool
{
if ($result->failureType === JournalApiResult::FAILURE_AUTH) {
Log::warning('[HubSpot Journal Polling] Auth failure, will retry next cycle', [
'context' => $context,
]);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_RATE_LIMIT) {
$waitSeconds = min($result->retryAfterSeconds ?? 60, self::MAX_BACKOFF_SECONDS);
Log::warning('[HubSpot Journal Polling] Rate limit hit, waiting', [
'wait_seconds' => $waitSeconds,
]);
$this->responsiveSleep($waitSeconds);
return false;
}
if ($result->failureType === JournalApiResult::FAILURE_OFFSET_EXPIRED) {
Log::warning('[HubSpot Journal Polling] Offset expired, resetting to fetch latest data', [
'expired_offset' => $context,
]);
$this->resetOffset();
$this->resetDbOffset();
return false;
}
if ($result->isPermanentFailure()) {
Log::error('[HubSpot Journal Polling] Permanent API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->shouldStop = true;
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable API failure', [
'context' => $context,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Clean up polling session resources
*/
private function cleanupPollingSession(): void
{
$this->logPollingEnd();
$this->storeOffsetInDB();
$this->releasePollingLock();
$this->apiClient->clearPortalCache();
$this->resetPollingState();
}
private function storeOffsetInDB(): void
{
// Save current offset to database before cleanup
$currentOffset = $this->getCurrentOffset(cacheOnly: true);
if ($currentOffset) {
$this->updateDbOffset($currentOffset);
Log::info('[HubSpot Journal Polling] Saved offset to database on cleanup', [
'offset' => $currentOffset,
]);
}
}
private function handleDownloadResult(JournalApiResult $result, string $offset, int $retryCount): ?bool
{
if ($result->failureType === JournalApiResult::FAILURE_UNKNOWN_PORTAL) {
$this->otherPortalSkipped++;
$this->updateOffset($offset);
return null;
}
if ($result->isPermanentFailure()) {
Log::warning('[HubSpot Journal Polling] Permanent failure, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'error' => $result->errorMessage,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::error('[HubSpot Journal Polling] Max retries reached, skipping offset', [
'offset' => $offset,
'portal_id' => $result->portalId,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
]);
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::warning('[HubSpot Journal Polling] Retryable failure, will retry', [
'offset' => $offset,
'failure_type' => $result->failureType,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
]);
$this->offsetRetryCount[$offset] = $retryCount;
if ($result->retryAfterSeconds) {
sleep(min($result->retryAfterSeconds, self::MAX_BACKOFF_SECONDS));
}
return false;
}
/**
* Handle transformation failure with retry logic
*/
private function handleTransformationFailure(string $offset, int $retryCount, \Exception $exception): bool
{
$retryCount++;
if ($retryCount >= self::MAX_OFFSET_RETRIES) {
Log::critical('[HubSpot Journal Polling] Max transformation retries reached - SKIPPING offset', [
'offset' => $offset,
'retry_count' => $retryCount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Skip this offset to prevent infinite loop with corrupted data
$this->updateOffset($offset);
$this->clearOffsetRetryCount($offset);
return false;
}
Log::error('[HubSpot Journal Polling] Transformation failed - will retry', [
'offset' => $offset,
'retry_count' => $retryCount,
'max_retries' => self::MAX_OFFSET_RETRIES,
'error' => $exception->getMessage(),
]);
$this->offsetRetryCount[$offset] = $retryCount;
return false;
}
/**
* Clear retry count for an offset after successful processing
*/
private function clearOffsetRetryCount(string $offset): void
{
unset($this->offsetRetryCount[$offset]);
}
}
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
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.em...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
5604
|
208
|
23
|
2026-05-07T15:58:08.873075+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778169488873_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotClientInterface.php
|
1
|
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
71
2
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;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
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;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Jobs\Crm\NoteObject;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Factory;
use HubSpot\Discovery\Discovery;
interface HubspotClientInterface extends ClientInterface
{
public function getInstance(): Factory;
public function getNewInstance(): Discovery;
public function getEngagementData(string $engagementId): array;
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string;
public function createMeeting(array $payload): Response;
public function getPaginatedData(array $payload, string $type, int $offset = 0): array;
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator;
public function getAccountById(string $crmId, array $fields): array;
public function getContactById(string $crmId, array $fields): array;
public function getOpportunitiesByIds(array $crmIds, array $fields): array;
public function getCompaniesByIds(array $crmIds, array $fields): array;
public function getContactsByIds(array $crmIds, array $fields): array;
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;
public function getOwners(): array;
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
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.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","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":"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":"71","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00930851,"height":0.0},"on_screen":false,"role_description":"text"},{"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":"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 public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\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 $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $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 public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\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 $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $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":"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":"1","depth":4,"bounds":{"left":0.39760637,"top":0.19952115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.19792499,"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.41389626,"top":0.19792499,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","depth":4,"bounds":{"left":0.11968085,"top":0.1963288,"width":0.3011968,"height":0.8036712},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","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}]...
|
2173538974204815661
|
6379320134023907428
|
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
71
2
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;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
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;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Jobs\Crm\NoteObject;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Factory;
use HubSpot\Discovery\Discovery;
interface HubspotClientInterface extends ClientInterface
{
public function getInstance(): Factory;
public function getNewInstance(): Discovery;
public function getEngagementData(string $engagementId): array;
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string;
public function createMeeting(array $payload): Response;
public function getPaginatedData(array $payload, string $type, int $offset = 0): array;
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator;
public function getAccountById(string $crmId, array $fields): array;
public function getContactById(string $crmId, array $fields): array;
public function getOpportunitiesByIds(array $crmIds, array $fields): array;
public function getCompaniesByIds(array $crmIds, array $fields): array;
public function getContactsByIds(array $crmIds, array $fields): array;
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;
public function getOwners(): array;
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
5603
|
207
|
16
|
2026-05-07T15:57:51.335595+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778169471335_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotClientInterface.php
|
1
|
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
2
71
2
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;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
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;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Jobs\Crm\NoteObject;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Factory;
use HubSpot\Discovery\Discovery;
interface HubspotClientInterface extends ClientInterface
{
public function getInstance(): Factory;
public function getNewInstance(): Discovery;
public function getEngagementData(string $engagementId): array;
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string;
public function createMeeting(array $payload): Response;
public function getPaginatedData(array $payload, string $type, int $offset = 0): array;
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator;
public function getAccountById(string $crmId, array $fields): array;
public function getContactById(string $crmId, array $fields): array;
public function getOpportunitiesByIds(array $crmIds, array $fields): array;
public function getCompaniesByIds(array $crmIds, array $fields): array;
public function getContactsByIds(array $crmIds, array $fields): array;
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;
public function getOwners(): array;
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
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<br/>Some incoming commits are not fetched<br/>","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":"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":"71","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.019444445,"height":0.02111111},"on_screen":false,"role_description":"text"},{"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":"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 public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\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 $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $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 public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\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 $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $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":"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":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","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}]...
|
5104323009125083167
|
6379320134023907428
|
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
2
71
2
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;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
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;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Jobs\Crm\NoteObject;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Factory;
use HubSpot\Discovery\Discovery;
interface HubspotClientInterface extends ClientInterface
{
public function getInstance(): Factory;
public function getNewInstance(): Discovery;
public function getEngagementData(string $engagementId): array;
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string;
public function createMeeting(array $payload): Response;
public function getPaginatedData(array $payload, string $type, int $offset = 0): array;
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator;
public function getAccountById(string $crmId, array $fields): array;
public function getContactById(string $crmId, array $fields): array;
public function getOpportunitiesByIds(array $crmIds, array $fields): array;
public function getCompaniesByIds(array $crmIds, array $fields): array;
public function getContactsByIds(array $crmIds, array $fields): array;
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;
public function getOwners(): array;
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
5601
|
NULL
|
NULL
|
NULL
|
|
5602
|
208
|
22
|
2026-05-07T15:57:51.235957+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778169471235_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotClientInterface.php
|
1
|
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
71
2
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;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
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;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Jobs\Crm\NoteObject;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Factory;
use HubSpot\Discovery\Discovery;
interface HubspotClientInterface extends ClientInterface
{
public function getInstance(): Factory;
public function getNewInstance(): Discovery;
public function getEngagementData(string $engagementId): array;
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string;
public function createMeeting(array $payload): Response;
public function getPaginatedData(array $payload, string $type, int $offset = 0): array;
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator;
public function getAccountById(string $crmId, array $fields): array;
public function getContactById(string $crmId, array $fields): array;
public function getOpportunitiesByIds(array $crmIds, array $fields): array;
public function getCompaniesByIds(array $crmIds, array $fields): array;
public function getContactsByIds(array $crmIds, array $fields): array;
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;
public function getOwners(): array;
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
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.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","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":"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":"71","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00930851,"height":0.0},"on_screen":false,"role_description":"text"},{"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":"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 public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\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 $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $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 public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\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 $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $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":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","depth":4,"bounds":{"left":0.11968085,"top":0.1963288,"width":0.30219415,"height":0.8036712},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","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}]...
|
5104323009125083167
|
6379320134023907428
|
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
2
71
2
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;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
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;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Jobs\Crm\NoteObject;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Factory;
use HubSpot\Discovery\Discovery;
interface HubspotClientInterface extends ClientInterface
{
public function getInstance(): Factory;
public function getNewInstance(): Discovery;
public function getEngagementData(string $engagementId): array;
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string;
public function createMeeting(array $payload): Response;
public function getPaginatedData(array $payload, string $type, int $offset = 0): array;
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator;
public function getAccountById(string $crmId, array $fields): array;
public function getContactById(string $crmId, array $fields): array;
public function getOpportunitiesByIds(array $crmIds, array $fields): array;
public function getCompaniesByIds(array $crmIds, array $fields): array;
public function getContactsByIds(array $crmIds, array $fields): array;
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;
public function getOwners(): array;
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
5600
|
NULL
|
NULL
|
NULL
|
|
5484
|
199
|
16
|
2026-05-07T15:37:30.246594+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778168250246_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotClientInterface.php
|
1
|
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
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] string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
Project
Project...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","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":"Execute","depth":4,"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,"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,"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,"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,"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,"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,"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,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","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":"# **************************** 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.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":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","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}]...
|
-9104836138597588933
|
919859931228061261
|
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] string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
Project
Project...
|
5482
|
NULL
|
NULL
|
NULL
|
|
5483
|
200
|
18
|
2026-05-07T15:37:30.246563+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778168250246_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotClientInterface.php
|
1
|
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] string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
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.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","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.33843085,"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.34707448,"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.35804522,"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.36668882,"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.37533244,"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.3863032,"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.39727393,"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.42386967,"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.4348404,"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.6821808,"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.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"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.6974734,"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.70478725,"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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","depth":4,"bounds":{"left":0.11968085,"top":0.2952913,"width":0.26662233,"height":0.7047087},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","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}]...
|
6385138748352995160
|
2072780336323280461
|
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] string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
5481
|
NULL
|
NULL
|
NULL
|
|
5417
|
196
|
7
|
2026-05-07T15:26:21.983670+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167581983_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotClientInterface.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormcodeFV faVsco.jsProledeyT DeleteCrmEntityT PhostormcodeFV faVsco.jsProledeyT DeleteCrmEntityTrait.phpC Huosporweonoo(9) RateLimitexception.pnpv D Pagination© HubspotPaginatC PaginationconticT OpportunitySyncTrait.phpc) Hubspotsinglesyncstrategy.pngC) Paginationstate•_ Prospectsearchstr> 0 RedisImportBatchJobTrait.pho(c) Hubspot/Service.php(C) PayloadBuilder.pho© MatchActivityCrmData.phpv D ServiceTraitsC) CrmActivityService.phoCachedCrmServiceDecorator.php•Hubspot/....SyncCrmEntitiesTrait.phpC) Pipedrive/Service.php+Opportunitvsyn1Servicelnterface.ohd+ SyncermEntitiesT SuncFieldstrait.T Writecrmtrait.p•DUtsdeclare(strict_types=1)• Weonook© BatchSvncCollectol 5C) BatchSvncRedisSer 0© Client.phpC) ClosedDealStadesS 8use Jiminhy cxcepctons kateLimicexcepcion.ACceptRejectG DealFieldsService.puse Jiminny Joos urm Noreud ect(c) DecorateActivitv nt 10use sevenonores nuospor cxceptions nubspoccxcepcionC) SioldDefinitions nhr 11use sevenshores Hubspot Htcp kesponse:© FieldTypeConvertei 12use sevenshores Hubspot ractory(0 HubsnotClientinterl 13use Hubspot Discovery Discovery:(C) HubsnotTokenMan: 14© PayloadBuilder.php 15 Ctintertace Hubspotullentintertace extends Cllentintertacee DomotoCrmObioctl 11o• ResponseNormalize 17 Coubuc tunction oet instanceu: ractory:public function detrewinstanced: Discovery:c) service.ono© SyncFieldAction.ph© SyncRelatedActivits 19 Cupublic function oetEngagementData(strina Sengagementd): arrav:c) WebhooksyncBatcno usages• Ca IntegrationAppnubuic function createvoted› Accessorsstrina SownerId.›D ADI• contioint Stimestamn.MDTOstrina SobnectIdi•D FiltersNote0bnect Snote0biectaobs): ?string;> M ProspectSearchStri 27 Clpublic function createMeeting(array $payload): Response;v ServiceTraitspublic function getPaginatedData(array Spayload, string $type, int $offset = 0): array:TSyternalManstir4 usages() InternalAccounts 29 Glpublic function getPaginatedDataGenerator(M LavoutTrait nhnarray Spayloadscring scype@otSunnortedTr 52int Soffset = 0,int &Stotal = 0estring oslasckecorald = nuluF1 33m 20s155 CR yncCrmMetada 55): \Generator:Trim & encode video (*E) PystemStateTrapublic function getAccountById(string ScrmId, array Sfields): array:© DecorateActivity.pr 37 Cupublic function GetcontacfytdX Reject File 129- arfof 4 files →arQube for INE suaa.ons. Deteat.more securitiscuecin.wour.DLD.files//Tin/Sonar@ube Cloud.for.free//[EMAIL]/leam.more_//Donit.ask.again./itodav 10:25)= custom.logElaravel.logA SF (jiminny@localhost]4 HS_local (iminny@localhost] >iti accounts (jiminny@localhost]A console (PROD]# console [eu)A console [STAGING]Tx: Autov liminnysELEC * FRoM crm tield data WHERE crm Layout entity 1d IN (6494.6495.6496.6497.6498,649907wieoetwos37 VSELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,u.emarsa.*t.owner 1d FROM social accounts saJOIN users uon U.id = sa.sociable 1dJOIN teams t l..n<->1: on t.id = u.team_idWHERE U.team_id = 2 and sa.provider = 'hubspot';select * from social accounts whene id = 1409•select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm_ provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:select * trom crm tleld data where obnect 10 = 15057SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):select * from team features where team id IN (1):select * from team features where feature id IN (36):SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm provider 1d = 111751'4"Ssiud & "HURSPOT WERHOOK SYNCI# $feature = Feature::query(->where('slug', $slug)->firstO:# TeanFeature::quenv()->create(['feature id' => Sfeature->aetId() Iteam id' => Steam->aetId@l)•# hubsnot wehhook metricslselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•select * from activities where crm confiquration id = 2:select settings from crm confiqurations where id = 11;100% 12Thu 7 May 18:26:22U AskJiminnyReportActivityServiceTest vCascadeHubspot Rate Limiting, Hubspot Search Ra+0 ..lets move all search request to client so we can wrap it in rate limit logicThought for 1sl'll refactor the code to centralize all search requests in the Client class with proper rate limiting. Let me first examine the current Clientimplementation to understand the pattern.Read Client.oho #L1-120Thought for 1sRead Client.oho and HubsootClientinterface.oho |Poad Client nhn #l 220.219|Now l'll implement the reractoring. Here's my plan:1. Add search() method to Client.php that wraps search API calls in rate limiting2. Add method sianature to HubspotClientinterface.pho3. Undate HubsnotPadinationService. nhn to use the new method4. Update Service.php search methods to use the new methodIet me imnlement these changes.HubspotClientinterface.php• HubspotClientinterface.phpAsk anvthina (&4D+ @ CodelC° Adantive* Reiect allAccent alliQWN Windsurf Teams8-1UTE.8io 4 spaces...
|
NULL
|
7336847112920611949
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormcodeFV faVsco.jsProledeyT DeleteCrmEntityT PhostormcodeFV faVsco.jsProledeyT DeleteCrmEntityTrait.phpC Huosporweonoo(9) RateLimitexception.pnpv D Pagination© HubspotPaginatC PaginationconticT OpportunitySyncTrait.phpc) Hubspotsinglesyncstrategy.pngC) Paginationstate•_ Prospectsearchstr> 0 RedisImportBatchJobTrait.pho(c) Hubspot/Service.php(C) PayloadBuilder.pho© MatchActivityCrmData.phpv D ServiceTraitsC) CrmActivityService.phoCachedCrmServiceDecorator.php•Hubspot/....SyncCrmEntitiesTrait.phpC) Pipedrive/Service.php+Opportunitvsyn1Servicelnterface.ohd+ SyncermEntitiesT SuncFieldstrait.T Writecrmtrait.p•DUtsdeclare(strict_types=1)• Weonook© BatchSvncCollectol 5C) BatchSvncRedisSer 0© Client.phpC) ClosedDealStadesS 8use Jiminhy cxcepctons kateLimicexcepcion.ACceptRejectG DealFieldsService.puse Jiminny Joos urm Noreud ect(c) DecorateActivitv nt 10use sevenonores nuospor cxceptions nubspoccxcepcionC) SioldDefinitions nhr 11use sevenshores Hubspot Htcp kesponse:© FieldTypeConvertei 12use sevenshores Hubspot ractory(0 HubsnotClientinterl 13use Hubspot Discovery Discovery:(C) HubsnotTokenMan: 14© PayloadBuilder.php 15 Ctintertace Hubspotullentintertace extends Cllentintertacee DomotoCrmObioctl 11o• ResponseNormalize 17 Coubuc tunction oet instanceu: ractory:public function detrewinstanced: Discovery:c) service.ono© SyncFieldAction.ph© SyncRelatedActivits 19 Cupublic function oetEngagementData(strina Sengagementd): arrav:c) WebhooksyncBatcno usages• Ca IntegrationAppnubuic function createvoted› Accessorsstrina SownerId.›D ADI• contioint Stimestamn.MDTOstrina SobnectIdi•D FiltersNote0bnect Snote0biectaobs): ?string;> M ProspectSearchStri 27 Clpublic function createMeeting(array $payload): Response;v ServiceTraitspublic function getPaginatedData(array Spayload, string $type, int $offset = 0): array:TSyternalManstir4 usages() InternalAccounts 29 Glpublic function getPaginatedDataGenerator(M LavoutTrait nhnarray Spayloadscring scype@otSunnortedTr 52int Soffset = 0,int &Stotal = 0estring oslasckecorald = nuluF1 33m 20s155 CR yncCrmMetada 55): \Generator:Trim & encode video (*E) PystemStateTrapublic function getAccountById(string ScrmId, array Sfields): array:© DecorateActivity.pr 37 Cupublic function GetcontacfytdX Reject File 129- arfof 4 files →arQube for INE suaa.ons. Deteat.more securitiscuecin.wour.DLD.files//Tin/Sonar@ube Cloud.for.free//[EMAIL]/leam.more_//Donit.ask.again./itodav 10:25)= custom.logElaravel.logA SF (jiminny@localhost]4 HS_local (iminny@localhost] >iti accounts (jiminny@localhost]A console (PROD]# console [eu)A console [STAGING]Tx: Autov liminnysELEC * FRoM crm tield data WHERE crm Layout entity 1d IN (6494.6495.6496.6497.6498,649907wieoetwos37 VSELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,u.emarsa.*t.owner 1d FROM social accounts saJOIN users uon U.id = sa.sociable 1dJOIN teams t l..n<->1: on t.id = u.team_idWHERE U.team_id = 2 and sa.provider = 'hubspot';select * from social accounts whene id = 1409•select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm_ provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:select * trom crm tleld data where obnect 10 = 15057SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):select * from team features where team id IN (1):select * from team features where feature id IN (36):SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm provider 1d = 111751'4"Ssiud & "HURSPOT WERHOOK SYNCI# $feature = Feature::query(->where('slug', $slug)->firstO:# TeanFeature::quenv()->create(['feature id' => Sfeature->aetId() Iteam id' => Steam->aetId@l)•# hubsnot wehhook metricslselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•select * from activities where crm confiquration id = 2:select settings from crm confiqurations where id = 11;100% 12Thu 7 May 18:26:22U AskJiminnyReportActivityServiceTest vCascadeHubspot Rate Limiting, Hubspot Search Ra+0 ..lets move all search request to client so we can wrap it in rate limit logicThought for 1sl'll refactor the code to centralize all search requests in the Client class with proper rate limiting. Let me first examine the current Clientimplementation to understand the pattern.Read Client.oho #L1-120Thought for 1sRead Client.oho and HubsootClientinterface.oho |Poad Client nhn #l 220.219|Now l'll implement the reractoring. Here's my plan:1. Add search() method to Client.php that wraps search API calls in rate limiting2. Add method sianature to HubspotClientinterface.pho3. Undate HubsnotPadinationService. nhn to use the new method4. Update Service.php search methods to use the new methodIet me imnlement these changes.HubspotClientinterface.php• HubspotClientinterface.phpAsk anvthina (&4D+ @ CodelC° Adantive* Reiect allAccent alliQWN Windsurf Teams8-1UTE.8io 4 spaces...
|
5416
|
NULL
|
NULL
|
NULL
|
|
5416
|
196
|
6
|
2026-05-07T15:26:09.881737+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167569881_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotClientInterface.php
|
1
|
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] string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
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.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","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.39660904,"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.40525267,"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.4162234,"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.42486703,"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.43351063,"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.44448137,"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.4554521,"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.4820479,"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.49301863,"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.6821808,"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.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"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.6974734,"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.70478725,"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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Factory;\nuse HubSpot\\Discovery\\Discovery;\n\ninterface HubspotClientInterface extends ClientInterface\n{\n public function getInstance(): Factory;\n public function getNewInstance(): Discovery;\n public function getEngagementData(string $engagementId): array;\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string;\n public function createMeeting(array $payload): Response;\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array;\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 public function getAccountById(string $crmId, array $fields): array;\n public function getContactById(string $crmId, array $fields): array;\n public function getOpportunitiesByIds(array $crmIds, array $fields): array;\n public function getCompaniesByIds(array $crmIds, array $fields): array;\n public function getContactsByIds(array $crmIds, array $fields): array;\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array;\n public function getOwners(): array;\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array;\n}","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}]...
|
5693104104059166337
|
919859931228126797
|
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] string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array;
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
5415
|
196
|
5
|
2026-05-07T15:26:06.867441+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167566867_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotClientInterface.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormcodeFV faVsco.jsProiectRematchActivityOnCr PhostormcodeFV faVsco.jsProiectRematchActivityOnCrmObjectDetach.phpT DeleteCrmEntityTrait.php© HubspotWebhoc(A RateLimitException.pnpO Cllentong( ResponseException.png© HubspotPaginationService.phpv @ Pagination© HubspotPaginatC Paginationcontic (©) basicapi.pngbadkequest.phg*Hubspotexception.pnpT OpportunitySyncTrait.phpc) Hubspotsinglesyncstrategy.pngC) PaginationstateHubspotwebhookbatchsyncstrateav.ong© ImportOpportunityBatch.php•_ Prospectsearchstr> 0 Redis(ImportBatchJobTrait.php(c) Hubspot/Service.php() PayloadBuilder.phpC) Companies.php© MatchActivityCrmData.phpv D ServiceTraits(C) CrmActivityService.ohc(C) CachedCrmServiceDecorator.phpTHubspot/.SyncCrmEntitiesTrait.php(C) Pipedrive/Service.php+Opportunitvsyn1Servicelnterface.ohd@ OpportunitvSvncTest.ohn+ SyncermEntitiesT SuncFieldstrait.T Writecrmtrait.p•DUuls•WeonookC) BatchSvncCollectoC) BatchSvncRedisSe© Client.phpC) ClosedDealStadessG DealFieldsService.p© DecorateActivity.ph(C) SioldDefinitions nhrC) SieldTvneConverte0 HubsnotClientinter(C) HubsnotTokenMan© PayloadBuilder.phpG DomotoCrmOhiontl0 DocnancaMormoliz!c) service.ono© SyncFieldAction.ph© SyncRelatedActivitc) WebhooksyncBatc• C IntegrationApp› Accessors›D ADI• contio> MDTO•D Filters•Hlobs• ProspectSearchStr.v ServiceTraitsTSyternalManstirt internalAccount? LavoutTrait nhneotSunnortedTrincCrmEntitiedincCrmFieldsTF1 33m 20s155 CR yncCrmMetadaTrim & encode video (2E) jystemStateTra© DataClient.php© DecorateActivity.phSonarQube for INF suadections: Netect more cecurity iccuec in vour DHP filec II Try SonarQube Cloud for fres II D.erver |l Learn more /l Don't ack adain (todav 10-25)= custom.log=laravel.logA SF (jiminny@localhost]4 HS_local (iminny@localhost] >iti accounts (jiminny@localhost]A console (PROD]# console [eu)A console [STAGING]Tx: Autovav owne liminnySELECT * FRUM crm_tieLd_data whERE crm_layout_entity_1d LN (6494,6495,6496,6497,6498,0499); m 06 A1 x6 ^37 VSELECTCONCAT(u.id, CASE WHEN .id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,u.emarsa.*t.owner 1d FROM social accounts saJOIN users Uon U.id = sa.sociable 1dJOIN teams t l..n<->1: on t.id = u.team_idWHERE U.team_id = 2 and sa.provider = 'hubspot';select * from social accounts whene id = 1409•select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm_ provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:select * trom crm tleld data where obnect 10 = 15057SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):select * from team features where team id IN (1):select * from team features where feature id IN (36):SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contacts WHERE crm provider 1d = 111751'4"Ssiud & "HURSPOT WERHOOK SYNC"# Steam = Jiminnv Models Team: • find(2)•# $feature = Feature::query(->where('slug', $slug)->firstO:# TeanFeature::quenv()->create(['feature id' => Sfeature->aetId() Iteam id' => Steam->aetId@l)•# hubsnot wehhook metricslselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414':SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•select * from activities where crm confiquration id = 2:select settings from crm confiqurations where id = 11;100% LzThu 7 May 18:26:07AskJiminnyReportActivityServiceTest vCascadeHubspot Rate Limiting, Hubspot Search Ra+0 ..lets move all search request to client so we can wrap it in rate limit logicI'll refactor the code to centralize all search requests in the Client class with proper rate limiting. Let me first examine the current Clientimplementation to understand the pattern.Read Client.oho #L1-120Read Client.php and HubspotClientinterface.php ›Read Client.php #L220-319Thouaht for 1sNow l'll create a comprehensive refactoring plan. Let me check the current search methods in Service.pho:Now l'll implement the reractoring. Here's my plan:1. Add search() method to Client.php that wraps search API calls in rate limiting2. Add method sianature to HubspotClientinterface.oho2. Undate HubsnotPaainationService. nhn to uce the new method4. Uodate Service, oho search methods to use the new methodlet me imnlement these chanaes.• HubsootClientinterface.ohvl≤ 261 tokensAsk anvthina (4D)C° Adantive• QW Windsurf Toams 126-66UTF.8io 4 spaces...
|
NULL
|
-3002157307501164083
|
NULL
|
idle
|
ocr
|
NULL
|
PhostormcodeFV faVsco.jsProiectRematchActivityOnCr PhostormcodeFV faVsco.jsProiectRematchActivityOnCrmObjectDetach.phpT DeleteCrmEntityTrait.php© HubspotWebhoc(A RateLimitException.pnpO Cllentong( ResponseException.png© HubspotPaginationService.phpv @ Pagination© HubspotPaginatC Paginationcontic (©) basicapi.pngbadkequest.phg*Hubspotexception.pnpT OpportunitySyncTrait.phpc) Hubspotsinglesyncstrategy.pngC) PaginationstateHubspotwebhookbatchsyncstrateav.ong© ImportOpportunityBatch.php•_ Prospectsearchstr> 0 Redis(ImportBatchJobTrait.php(c) Hubspot/Service.php() PayloadBuilder.phpC) Companies.php© MatchActivityCrmData.phpv D ServiceTraits(C) CrmActivityService.ohc(C) CachedCrmServiceDecorator.phpTHubspot/.SyncCrmEntitiesTrait.php(C) Pipedrive/Service.php+Opportunitvsyn1Servicelnterface.ohd@ OpportunitvSvncTest.ohn+ SyncermEntitiesT SuncFieldstrait.T Writecrmtrait.p•DUuls•WeonookC) BatchSvncCollectoC) BatchSvncRedisSe© Client.phpC) ClosedDealStadessG DealFieldsService.p© DecorateActivity.ph(C) SioldDefinitions nhrC) SieldTvneConverte0 HubsnotClientinter(C) HubsnotTokenMan© PayloadBuilder.phpG DomotoCrmOhiontl0 DocnancaMormoliz!c) service.ono© SyncFieldAction.ph© SyncRelatedActivitc) WebhooksyncBatc• C IntegrationApp› Accessors›D ADI• contio> MDTO•D Filters•Hlobs• ProspectSearchStr.v ServiceTraitsTSyternalManstirt internalAccount? LavoutTrait nhneotSunnortedTrincCrmEntitiedincCrmFieldsTF1 33m 20s155 CR yncCrmMetadaTrim & encode video (2E) jystemStateTra© DataClient.php© DecorateActivity.phSonarQube for INF suadections: Netect more cecurity iccuec in vour DHP filec II Try SonarQube Cloud for fres II D.erver |l Learn more /l Don't ack adain (todav 10-25)= custom.log=laravel.logA SF (jiminny@localhost]4 HS_local (iminny@localhost] >iti accounts (jiminny@localhost]A console (PROD]# console [eu)A console [STAGING]Tx: Autovav owne liminnySELECT * FRUM crm_tieLd_data whERE crm_layout_entity_1d LN (6494,6495,6496,6497,6498,0499); m 06 A1 x6 ^37 VSELECTCONCAT(u.id, CASE WHEN .id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,u.emarsa.*t.owner 1d FROM social accounts saJOIN users Uon U.id = sa.sociable 1dJOIN teams t l..n<->1: on t.id = u.team_idWHERE U.team_id = 2 and sa.provider = 'hubspot';select * from social accounts whene id = 1409•select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm_ provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:select * trom crm tleld data where obnect 10 = 15057SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):select * from team features where team id IN (1):select * from team features where feature id IN (36):SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contacts WHERE crm provider 1d = 111751'4"Ssiud & "HURSPOT WERHOOK SYNC"# Steam = Jiminnv Models Team: • find(2)•# $feature = Feature::query(->where('slug', $slug)->firstO:# TeanFeature::quenv()->create(['feature id' => Sfeature->aetId() Iteam id' => Steam->aetId@l)•# hubsnot wehhook metricslselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414':SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•select * from activities where crm confiquration id = 2:select settings from crm confiqurations where id = 11;100% LzThu 7 May 18:26:07AskJiminnyReportActivityServiceTest vCascadeHubspot Rate Limiting, Hubspot Search Ra+0 ..lets move all search request to client so we can wrap it in rate limit logicI'll refactor the code to centralize all search requests in the Client class with proper rate limiting. Let me first examine the current Clientimplementation to understand the pattern.Read Client.oho #L1-120Read Client.php and HubspotClientinterface.php ›Read Client.php #L220-319Thouaht for 1sNow l'll create a comprehensive refactoring plan. Let me check the current search methods in Service.pho:Now l'll implement the reractoring. Here's my plan:1. Add search() method to Client.php that wraps search API calls in rate limiting2. Add method sianature to HubspotClientinterface.oho2. Undate HubsnotPaainationService. nhn to uce the new method4. Uodate Service, oho search methods to use the new methodlet me imnlement these chanaes.• HubsootClientinterface.ohvl≤ 261 tokensAsk anvthina (4D)C° Adantive• QW Windsurf Toams 126-66UTF.8io 4 spaces...
|
5407
|
NULL
|
NULL
|
NULL
|
|
3643
|
134
|
15
|
2026-05-07T12:32:32.603482+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778157152603_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/…/SyncCrmEntitiesTrait.php
|
1
|
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
Search History
Hubspot
New Line
Replace History
Replace
New Line
Previous Occurrence
Next Occurrence
Search All
Search Backward
Search Forward
Match сase
Words
Regex
?
Close
[2026-05-07 12:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:10] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"172d1ae8-b8cc-4804-bed9-e32d074e265c","trace_id":"4817cdef-8d3e-4914-8bee-feffb18efe1b"}
[2026-05-07 12:28:14] 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":"172d1ae8-b8cc-4804-bed9-e32d074e265c","trace_id":"4817cdef-8d3e-4914-8bee-feffb18efe1b"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1499,"provider":"hubspot","refreshToken":"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86","state":"connected"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.NOTICE: Monitoring start {"correlation_id":"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5","trace_id":"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72"}
[2026-05-07 12:28:18] local.NOTICE: Monitoring end {"correlation_id":"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5","trace_id":"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountService] Token refreshed {"socialAccountId":1499,"provider":"hubspot","state":"connected"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:19] local.INFO: [Hubspot] Failed to fetch opportunity {"crm_id":"374720564","reason":"[429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)
"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:19] local.ERROR: [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{"status":"error","message":"You have reached your ten_secondly_rolling limit.","errorType":"RATE_LIMIT","correlationId" (truncated...)
{"exception":"[object] (HubSpot\\Client\\Crm\\Deals\\ApiException(code: 429): [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)
at /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php(676): HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi->getByIdWithHttpInfo('374720564', 'hs_object_id,de...', 'companies,conta...', false, NULL)
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(212): HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi->getById('374720564', 'hs_object_id,de...', 'companies,conta...')
#2 /home/jiminny/app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php(130): Jiminny\\Services\\Crm\\Hubspot\\Client->getOpportunityById('374720564', Array)
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(351): Jiminny\\Services\\Crm\\Hubspot\\Service->syncOpportunity('374720564')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:20] 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":"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d","trace_id":"1ea62b83-9639-41e3-a523-26404c39fa80"}
[2026-05-07 12:28:20] 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":"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d","trace_id":"1ea62b83-9639-41e3-a523-26404c39fa80"}
[2026-05-07 12:28:24] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] 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":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:27] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"calendar:sync","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"3789f06a-4f0c-4b12-be80-d6a36e089d1b","trace_id":"6ce89147-1624-456d-9e75-f4948f2c5db8"}
[2026-05-07 12:28:30] local.NOTICE: Calendar sync start {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"3789f06a-4f0c-4b12-be80-d6a36e089d1b","trace_id":"6ce89147-1624-456d-9e75-f4948f2c5db8"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1393,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1393,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1393,"provider":"google","refreshToken":"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1387,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1387,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1387,"provider":"google","refreshToken":"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1348,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1348,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1348,"provider":"google","refreshToken":"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1361,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1361,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1361,"provider":"google","refreshToken":"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1310,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1310,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1310,"provider":"google","refreshToken":"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1333,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1333,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1333,"provider":"google","refreshToken":"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1368,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1368,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1368,"provider":"google","refreshToken":"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1365,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1365,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1365,"provider":"google","refreshToken":"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1364,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1364,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1364,"provider":"google","refreshToken":"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1370,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1370,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1370,"provider":"office","refreshToken":"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] 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: e890fdc1-dbe8-4a59-ae57-2af6bced3c00 Correlation ID: 57554cdf-16df-47f1-b0d9-5f0b8da37afe Timestamp: 2026-05-07 12:28:33Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:33Z\",\"trace_id\":\"e890fdc1-dbe8-4a59-ae57-2af6bced3c00\",\"correlation_id\":\"57554cdf-16df-47f1-b0d9-5f0b8da37afe\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1202,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1202,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1202,"provider":"office","refreshToken":"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] 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: 2a3e5b60-5770-46f2-aca6-7b0527363000 Correlation ID: 57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2 Timestamp: 2026-05-07 12:28:34Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:34Z\",\"trace_id\":\"2a3e5b60-5770-46f2-aca6-7b0527363000\",\"correlation_id\":\"57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: Calendar sync job dispatched {"calendar_id":501} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1300,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1300,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1300,"provider":"google","refreshToken":"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1409,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1409,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1409,"provider":"google","refreshToken":"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1352,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1352,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1352,"provider":"google","refreshToken":"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1296,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1296,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1296,"provider":"office","refreshToken":"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] 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":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"integration-app","crm_owner":1695,"team_id":3143} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] 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: 2e34e335-a76d-40e5-bc50-3861392e4c00 Correlation ID: 9db5b389-6165-4feb-8e6e-bedc369e1c87 Timestamp: 2026-05-07 12:28:35Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:35Z\",\"trace_id\":\"2e34e335-a76d-40e5-bc50-3861392e4c00\",\"correlation_id\":\"9db5b389-6165-4feb-8e6e-bedc369e1c87\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":391,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":391,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":391,"provider":"office","refreshToken":"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.INFO: [Google Calendar] Failed to watch channel for calendar {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.WARNING: [Calendar] Sync failed {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] 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: 6eba7173-b781-4e55-b1fb-1087ed023000 Correlation ID: 3086e346-c6eb-4f1c-8b1d-a477ce3821f4 Timestamp: 2026-05-07 12:28:36Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:36Z\",\"trace_id\":\"6eba7173-b781-4e55-b1fb-1087ed023000\",\"correlation_id\":\"3086e346-c6eb-4f1c-8b1d-a477ce3821f4\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1271,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1271,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1271,"provider":"office","refreshToken":"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1271,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided...
|
[{"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.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","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":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.53457445,"top":0.07980846,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Hubspot","depth":4,"bounds":{"left":0.5455452,"top":0.07980846,"width":0.11469415,"height":0.015961692},"on_screen":true,"value":"Hubspot","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.66921544,"top":0.07980846,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.68484044,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"bounds":{"left":0.69348407,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search All","depth":4,"bounds":{"left":0.7044548,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Backward","depth":4,"bounds":{"left":0.7130984,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Forward","depth":4,"bounds":{"left":0.72174203,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match сase","depth":4,"bounds":{"left":0.73038566,"top":0.07821229,"width":0.032247342,"height":0.01915403},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":4,"bounds":{"left":0.76263297,"top":0.07821229,"width":0.023271276,"height":0.01915403},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":4,"bounds":{"left":0.7859042,"top":0.07821229,"width":0.022938829,"height":0.01915403},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"?","depth":4,"bounds":{"left":0.8088431,"top":0.08220271,"width":0.0019946808,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.97539896,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 12:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:10] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"172d1ae8-b8cc-4804-bed9-e32d074e265c\",\"trace_id\":\"4817cdef-8d3e-4914-8bee-feffb18efe1b\"}\n[2026-05-07 12:28:14] 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\":\"172d1ae8-b8cc-4804-bed9-e32d074e265c\",\"trace_id\":\"4817cdef-8d3e-4914-8bee-feffb18efe1b\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"refreshToken\":\"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86\",\"state\":\"connected\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.NOTICE: Monitoring start {\"correlation_id\":\"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5\",\"trace_id\":\"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72\"}\n[2026-05-07 12:28:18] local.NOTICE: Monitoring end {\"correlation_id\":\"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5\",\"trace_id\":\"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"state\":\"connected\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:19] local.INFO: [Hubspot] Failed to fetch opportunity {\"crm_id\":\"374720564\",\"reason\":\"[429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your ten_secondly_rolling limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\" (truncated...)\n\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:19] local.ERROR: [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n {\"exception\":\"[object] (HubSpot\\\\Client\\\\Crm\\\\Deals\\\\ApiException(code: 429): [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your ten_secondly_rolling limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\" (truncated...)\n at /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php(676): HubSpot\\\\Client\\\\Crm\\\\Deals\\\\Api\\\\BasicApi->getByIdWithHttpInfo('374720564', 'hs_object_id,de...', 'companies,conta...', false, NULL)\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(212): HubSpot\\\\Client\\\\Crm\\\\Deals\\\\Api\\\\BasicApi->getById('374720564', 'hs_object_id,de...', 'companies,conta...')\n#2 /home/jiminny/app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php(130): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getOpportunityById('374720564', Array)\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(351): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->syncOpportunity('374720564')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:20] 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\":\"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d\",\"trace_id\":\"1ea62b83-9639-41e3-a523-26404c39fa80\"}\n[2026-05-07 12:28:20] 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\":\"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d\",\"trace_id\":\"1ea62b83-9639-41e3-a523-26404c39fa80\"}\n[2026-05-07 12:28:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] 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\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3789f06a-4f0c-4b12-be80-d6a36e089d1b\",\"trace_id\":\"6ce89147-1624-456d-9e75-f4948f2c5db8\"}\n[2026-05-07 12:28:30] local.NOTICE: Calendar sync start {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3789f06a-4f0c-4b12-be80-d6a36e089d1b\",\"trace_id\":\"6ce89147-1624-456d-9e75-f4948f2c5db8\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] 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: e890fdc1-dbe8-4a59-ae57-2af6bced3c00 Correlation ID: 57554cdf-16df-47f1-b0d9-5f0b8da37afe Timestamp: 2026-05-07 12:28:33Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:33Z\\\",\\\"trace_id\\\":\\\"e890fdc1-dbe8-4a59-ae57-2af6bced3c00\\\",\\\"correlation_id\\\":\\\"57554cdf-16df-47f1-b0d9-5f0b8da37afe\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] 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: 2a3e5b60-5770-46f2-aca6-7b0527363000 Correlation ID: 57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2 Timestamp: 2026-05-07 12:28:34Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:34Z\\\",\\\"trace_id\\\":\\\"2a3e5b60-5770-46f2-aca6-7b0527363000\\\",\\\"correlation_id\\\":\\\"57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] 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: 2e34e335-a76d-40e5-bc50-3861392e4c00 Correlation ID: 9db5b389-6165-4feb-8e6e-bedc369e1c87 Timestamp: 2026-05-07 12:28:35Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:35Z\\\",\\\"trace_id\\\":\\\"2e34e335-a76d-40e5-bc50-3861392e4c00\\\",\\\"correlation_id\\\":\\\"9db5b389-6165-4feb-8e6e-bedc369e1c87\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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: 6eba7173-b781-4e55-b1fb-1087ed023000 Correlation ID: 3086e346-c6eb-4f1c-8b1d-a477ce3821f4 Timestamp: 2026-05-07 12:28:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:36Z\\\",\\\"trace_id\\\":\\\"6eba7173-b781-4e55-b1fb-1087ed023000\\\",\\\"correlation_id\\\":\\\"3086e346-c6eb-4f1c-8b1d-a477ce3821f4\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] 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: b18cbf88-c6d0-4caa-9af9-d2dabb673500 Correlation ID: ead4f7c0-3077-42bb-84d0-c3b9a1432182 Timestamp: 2026-05-07 12:28:37Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:37Z\\\",\\\"trace_id\\\":\\\"b18cbf88-c6d0-4caa-9af9-d2dabb673500\\\",\\\"correlation_id\\\":\\\"ead4f7c0-3077-42bb-84d0-c3b9a1432182\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCHhwR3crxfEuMI8zGlf-bMYpCFtdxXvSJWTlnqQvu_jjoOrOYL2VG9rZwFHCERHxGfGEK3CmQX6x8MJG3ZbBXGuVIS6C7u-doY5maMRdsfnrHIAEMJd4Bs_WMfMH4tDJ8j9aul7DHDEJaP7w0PoPPpcoxu4nEk4vk-MolJBEgkSrayEewuBs5JVItUX9lUY2tA.yO2roNQ4Vdm6hBgoutuphGchuzbvsk7aqt5wHfcyeFQ\",\"last_sync\":\"2026-05-06 15:58:35\",\"dateMode\":\"daily\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:29:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"02cf8f7b-abd2-4e63-87bc-3e8fce928261\",\"trace_id\":\"6752601b-1c03-48ae-a590-323cc5ed601e\"}\n[2026-05-07 12:29:10] 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\":\"02cf8f7b-abd2-4e63-87bc-3e8fce928261\",\"trace_id\":\"6752601b-1c03-48ae-a590-323cc5ed601e\"}\n[2026-05-07 12:29:12] local.NOTICE: Monitoring start {\"correlation_id\":\"d3765bc2-52fa-4bc4-9c94-52822e08f7a5\",\"trace_id\":\"0299fb07-ac8c-4159-ad7e-c1bce0958d25\"}\n[2026-05-07 12:29:12] local.NOTICE: Monitoring end {\"correlation_id\":\"d3765bc2-52fa-4bc4-9c94-52822e08f7a5\",\"trace_id\":\"0299fb07-ac8c-4159-ad7e-c1bce0958d25\"}\n[2026-05-07 12:29:14] 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\":\"d59f6990-3c72-4f1b-b5fa-4035b1be1dbb\",\"trace_id\":\"d502f355-1716-4272-9dc2-79f8c70cbbf4\"}\n[2026-05-07 12:29:14] 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\":\"d59f6990-3c72-4f1b-b5fa-4035b1be1dbb\",\"trace_id\":\"d502f355-1716-4272-9dc2-79f8c70cbbf4\"}\n[2026-05-07 12:29:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] 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\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3aa037e0-524c-4f99-a2fe-9537e1034e93\",\"trace_id\":\"c8f2c739-13e2-490c-9bce-6b940c4073cc\"}\n[2026-05-07 12:30:08] 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\":\"3aa037e0-524c-4f99-a2fe-9537e1034e93\",\"trace_id\":\"c8f2c739-13e2-490c-9bce-6b940c4073cc\"}\n[2026-05-07 12:30:12] local.NOTICE: Monitoring start {\"correlation_id\":\"33c6a1f2-608d-47f9-a340-97a81d930e48\",\"trace_id\":\"e07fb9c3-cf43-4caa-bb94-08be684e8e00\"}\n[2026-05-07 12:30:12] local.NOTICE: Monitoring end {\"correlation_id\":\"33c6a1f2-608d-47f9-a340-97a81d930e48\",\"trace_id\":\"e07fb9c3-cf43-4caa-bb94-08be684e8e00\"}\n[2026-05-07 12:30:14] 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\":\"8da3e1c6-c806-4120-a0d8-8d99e640c4d3\",\"trace_id\":\"4813fc1e-67c7-495d-bafe-1c0bddb52670\"}\n[2026-05-07 12:30:14] 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\":\"8da3e1c6-c806-4120-a0d8-8d99e640c4d3\",\"trace_id\":\"4813fc1e-67c7-495d-bafe-1c0bddb52670\"}\n[2026-05-07 12:30:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] 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\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:28:00, 2026-05-07 12:30:00] {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:28:00, 2026-05-07 12:30:00] {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"36d2678a-b734-46bd-990e-9968b2fb86d5\",\"trace_id\":\"0f9e0c4b-3fbb-400e-97a8-5b59b1c18d50\"}\n[2026-05-07 12:30:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"36d2678a-b734-46bd-990e-9968b2fb86d5\",\"trace_id\":\"0f9e0c4b-3fbb-400e-97a8-5b59b1c18d50\"}\n[2026-05-07 12:30:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3559116e-0397-44eb-a839-e397bbd4c3f4\",\"trace_id\":\"e765565d-25d2-45ab-b9a4-42ef784a60fc\"}\n[2026-05-07 12:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3559116e-0397-44eb-a839-e397bbd4c3f4\",\"trace_id\":\"e765565d-25d2-45ab-b9a4-42ef784a60fc\"}\n[2026-05-07 12:30:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:27] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:28] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: Running conference:monitor:start command for activities in (2026-05-07 12:20:00, 2026-05-07 12:25:00] {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: [conference:monitor:start] No activities found in (2026-05-07 12:20:00, 2026-05-07 12:25:00] {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"12:25\",\"to\":\"12:30\"} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"02:20\",\"to\":\"02:25\"} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:32] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3fce27e4-1b07-40fc-8a53-fbad08b093e2\",\"trace_id\":\"3b8bc557-6e3e-42bf-853b-b7b38c986086\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4b772eb2-f1f8-46d1-afe2-8c7e5bc14182\",\"trace_id\":\"a1b68813-a4cf-4767-8a9f-dde19264b31d\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3fce27e4-1b07-40fc-8a53-fbad08b093e2\",\"trace_id\":\"3b8bc557-6e3e-42bf-853b-b7b38c986086\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-05-07T12:32:37.470385Z\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4b772eb2-f1f8-46d1-afe2-8c7e5bc14182\",\"trace_id\":\"a1b68813-a4cf-4767-8a9f-dde19264b31d\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"101dc452-3092-4c93-b585-b80bc8b44698\",\"trace_id\":\"b65e71f7-dad2-4432-9357-c7f05e39ae8f\"}\n[2026-05-07 12:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"101dc452-3092-4c93-b585-b80bc8b44698\",\"trace_id\":\"b65e71f7-dad2-4432-9357-c7f05e39ae8f\"}\n[2026-05-07 12:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812319,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812320,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812321,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812322,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812323,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812324,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] 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\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"salesforce\",\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"salesforce\",\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.ALERT: [SyncActivity] Failed {\"import_id\":812319,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Social account for Salesforce cannot be found. Please login to Jiminny to connect.\",\"file\":\"/home/jiminny/app/Services/Crm/BaseService.php\",\"line\":646} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":812320,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":812321,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":812322,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6c34890d-27f6-452c-918a-d61dfb22dc78\",\"trace_id\":\"2c6a1876-1e2d-4c90-a235-129bfc1fda30\"}\n[2026-05-07 12:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6c34890d-27f6-452c-918a-d61dfb22dc78\",\"trace_id\":\"2c6a1876-1e2d-4c90-a235-129bfc1fda30\"}\n[2026-05-07 12:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":812323,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SyncActivity] Start {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [HubSpot] Search calls for period {\"from\":\"2026-05-07 12:14:00\",\"to\":\"2026-05-07 12:30:00\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:45] local.INFO: [SyncActivity] End {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2,\"memory_usage\":28981120,\"memory_real_usage\":67108864,\"pid\":26154} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:47] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a3e015c8-7c91-4457-bff4-de94482186fc\",\"trace_id\":\"2966aada-d948-4fec-8388-30f39c48648f\"}\n[2026-05-07 12:30:47] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a3e015c8-7c91-4457-bff4-de94482186fc\",\"trace_id\":\"2966aada-d948-4fec-8388-30f39c48648f\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal Auth] Requesting new client credentials token {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: [HubSpot Journal Auth] Successfully obtained new access token {\"expires_in\":1800,\"cached_for\":1500} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c40e82b8-a5fd-4588-a637-9bd875fc31d9\",\"trace_id\":\"038436d0-e786-4dc4-98f2-f235d5e96a8d\"}\n[2026-05-07 12:30:48] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c40e82b8-a5fd-4588-a637-9bd875fc31d9\",\"trace_id\":\"038436d0-e786-4dc4-98f2-f235d5e96a8d\"}\n[2026-05-07 12:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] starting. {\"playlists\":[]} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] finished. {\"normalizedPlaylists\":[],\"deletedPlaylists\":[]} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31:03] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31: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\":\"8970c115-8149-440f-9afd-94a6f7890ed9\",\"trace_id\":\"fef587d7-b0e8-48f0-85b9-b224f5bdbecc\"}\n[2026-05-07 12:31: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\":\"8970c115-8149-440f-9afd-94a6f7890ed9\",\"trace_id\":\"fef587d7-b0e8-48f0-85b9-b224f5bdbecc\"}\n[2026-05-07 12:31:08] local.NOTICE: Monitoring start {\"correlation_id\":\"8d299958-4bdf-4f50-a904-b3f63d3b6edf\",\"trace_id\":\"8ac0c53d-8ecc-4e29-a465-ff8af18b3565\"}\n[2026-05-07 12:31:08] local.NOTICE: Monitoring end {\"correlation_id\":\"8d299958-4bdf-4f50-a904-b3f63d3b6edf\",\"trace_id\":\"8ac0c53d-8ecc-4e29-a465-ff8af18b3565\"}\n[2026-05-07 12:31:10] 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\":\"d384715e-b7f3-4ebd-be32-f9c6c9683c6a\",\"trace_id\":\"7221d562-a73d-4a09-8f67-a096ad834b43\"}\n[2026-05-07 12:31:10] 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\":\"d384715e-b7f3-4ebd-be32-f9c6c9683c6a\",\"trace_id\":\"7221d562-a73d-4a09-8f67-a096ad834b43\"}\n[2026-05-07 12:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] 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\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"daa2a14e-13d8-4e03-ade8-5cf235aff8af\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"daa2a14e-13d8-4e03-ade8-5cf235aff8af\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":25525984,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] 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\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":68.42,\"usage\":25519152,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":25558232,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] 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.49,\"average_seconds_per_request\":0.49} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":495.28} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":516.14,\"usage\":25681432,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":25659360,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":21.43,\"usage\":25505056,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":25543424,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":15.8,\"usage\":25498888,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] 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\":218.1,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:32:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"99620b99-af26-48c8-9376-70139e107fd6\",\"trace_id\":\"7da3af1b-da2d-4610-b72c-3aabcf9b27a7\"}\n[2026-05-07 12:32:09] 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\":\"99620b99-af26-48c8-9376-70139e107fd6\",\"trace_id\":\"7da3af1b-da2d-4610-b72c-3aabcf9b27a7\"}\n[2026-05-07 12:32:12] local.NOTICE: Monitoring start {\"correlation_id\":\"0be593b8-0aea-40cb-8dc9-1e74bb44ede1\",\"trace_id\":\"1537b6d8-4150-4d77-b2c6-9cec45a0374b\"}\n[2026-05-07 12:32:12] local.NOTICE: Monitoring end {\"correlation_id\":\"0be593b8-0aea-40cb-8dc9-1e74bb44ede1\",\"trace_id\":\"1537b6d8-4150-4d77-b2c6-9cec45a0374b\"}\n[2026-05-07 12:32:17] 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\":\"32c250b0-69ea-4669-98ae-b965a9479044\",\"trace_id\":\"d5d62398-e8b5-4dc7-96f0-4d945c921e2d\"}\n[2026-05-07 12:32:17] 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\":\"32c250b0-69ea-4669-98ae-b965a9479044\",\"trace_id\":\"d5d62398-e8b5-4dc7-96f0-4d945c921e2d\"}\n[2026-05-07 12:32:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] 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\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:30:00, 2026-05-07 12:32:00] {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:30:00, 2026-05-07 12:32:00] {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] 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\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:27] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"7ae3582d-c6f0-4d3f-a055-768d0b080b2a\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}","depth":4,"bounds":{"left":0.52859044,"top":0.016759777,"width":0.47140956,"height":0.98324025},"on_screen":true,"value":"[2026-05-07 12:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:10] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"172d1ae8-b8cc-4804-bed9-e32d074e265c\",\"trace_id\":\"4817cdef-8d3e-4914-8bee-feffb18efe1b\"}\n[2026-05-07 12:28:14] 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\":\"172d1ae8-b8cc-4804-bed9-e32d074e265c\",\"trace_id\":\"4817cdef-8d3e-4914-8bee-feffb18efe1b\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"refreshToken\":\"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86\",\"state\":\"connected\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.NOTICE: Monitoring start {\"correlation_id\":\"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5\",\"trace_id\":\"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72\"}\n[2026-05-07 12:28:18] local.NOTICE: Monitoring end {\"correlation_id\":\"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5\",\"trace_id\":\"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"state\":\"connected\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:19] local.INFO: [Hubspot] Failed to fetch opportunity {\"crm_id\":\"374720564\",\"reason\":\"[429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your ten_secondly_rolling limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\" (truncated...)\n\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:19] local.ERROR: [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n {\"exception\":\"[object] (HubSpot\\\\Client\\\\Crm\\\\Deals\\\\ApiException(code: 429): [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your ten_secondly_rolling limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\" (truncated...)\n at /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php(676): HubSpot\\\\Client\\\\Crm\\\\Deals\\\\Api\\\\BasicApi->getByIdWithHttpInfo('374720564', 'hs_object_id,de...', 'companies,conta...', false, NULL)\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(212): HubSpot\\\\Client\\\\Crm\\\\Deals\\\\Api\\\\BasicApi->getById('374720564', 'hs_object_id,de...', 'companies,conta...')\n#2 /home/jiminny/app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php(130): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getOpportunityById('374720564', Array)\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(351): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->syncOpportunity('374720564')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:20] 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\":\"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d\",\"trace_id\":\"1ea62b83-9639-41e3-a523-26404c39fa80\"}\n[2026-05-07 12:28:20] 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\":\"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d\",\"trace_id\":\"1ea62b83-9639-41e3-a523-26404c39fa80\"}\n[2026-05-07 12:28:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] 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\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3789f06a-4f0c-4b12-be80-d6a36e089d1b\",\"trace_id\":\"6ce89147-1624-456d-9e75-f4948f2c5db8\"}\n[2026-05-07 12:28:30] local.NOTICE: Calendar sync start {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3789f06a-4f0c-4b12-be80-d6a36e089d1b\",\"trace_id\":\"6ce89147-1624-456d-9e75-f4948f2c5db8\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] 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: e890fdc1-dbe8-4a59-ae57-2af6bced3c00 Correlation ID: 57554cdf-16df-47f1-b0d9-5f0b8da37afe Timestamp: 2026-05-07 12:28:33Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:33Z\\\",\\\"trace_id\\\":\\\"e890fdc1-dbe8-4a59-ae57-2af6bced3c00\\\",\\\"correlation_id\\\":\\\"57554cdf-16df-47f1-b0d9-5f0b8da37afe\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] 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: 2a3e5b60-5770-46f2-aca6-7b0527363000 Correlation ID: 57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2 Timestamp: 2026-05-07 12:28:34Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:34Z\\\",\\\"trace_id\\\":\\\"2a3e5b60-5770-46f2-aca6-7b0527363000\\\",\\\"correlation_id\\\":\\\"57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] 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: 2e34e335-a76d-40e5-bc50-3861392e4c00 Correlation ID: 9db5b389-6165-4feb-8e6e-bedc369e1c87 Timestamp: 2026-05-07 12:28:35Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:35Z\\\",\\\"trace_id\\\":\\\"2e34e335-a76d-40e5-bc50-3861392e4c00\\\",\\\"correlation_id\\\":\\\"9db5b389-6165-4feb-8e6e-bedc369e1c87\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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: 6eba7173-b781-4e55-b1fb-1087ed023000 Correlation ID: 3086e346-c6eb-4f1c-8b1d-a477ce3821f4 Timestamp: 2026-05-07 12:28:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:36Z\\\",\\\"trace_id\\\":\\\"6eba7173-b781-4e55-b1fb-1087ed023000\\\",\\\"correlation_id\\\":\\\"3086e346-c6eb-4f1c-8b1d-a477ce3821f4\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] 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: b18cbf88-c6d0-4caa-9af9-d2dabb673500 Correlation ID: ead4f7c0-3077-42bb-84d0-c3b9a1432182 Timestamp: 2026-05-07 12:28:37Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:37Z\\\",\\\"trace_id\\\":\\\"b18cbf88-c6d0-4caa-9af9-d2dabb673500\\\",\\\"correlation_id\\\":\\\"ead4f7c0-3077-42bb-84d0-c3b9a1432182\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCHhwR3crxfEuMI8zGlf-bMYpCFtdxXvSJWTlnqQvu_jjoOrOYL2VG9rZwFHCERHxGfGEK3CmQX6x8MJG3ZbBXGuVIS6C7u-doY5maMRdsfnrHIAEMJd4Bs_WMfMH4tDJ8j9aul7DHDEJaP7w0PoPPpcoxu4nEk4vk-MolJBEgkSrayEewuBs5JVItUX9lUY2tA.yO2roNQ4Vdm6hBgoutuphGchuzbvsk7aqt5wHfcyeFQ\",\"last_sync\":\"2026-05-06 15:58:35\",\"dateMode\":\"daily\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:29:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"02cf8f7b-abd2-4e63-87bc-3e8fce928261\",\"trace_id\":\"6752601b-1c03-48ae-a590-323cc5ed601e\"}\n[2026-05-07 12:29:10] 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\":\"02cf8f7b-abd2-4e63-87bc-3e8fce928261\",\"trace_id\":\"6752601b-1c03-48ae-a590-323cc5ed601e\"}\n[2026-05-07 12:29:12] local.NOTICE: Monitoring start {\"correlation_id\":\"d3765bc2-52fa-4bc4-9c94-52822e08f7a5\",\"trace_id\":\"0299fb07-ac8c-4159-ad7e-c1bce0958d25\"}\n[2026-05-07 12:29:12] local.NOTICE: Monitoring end {\"correlation_id\":\"d3765bc2-52fa-4bc4-9c94-52822e08f7a5\",\"trace_id\":\"0299fb07-ac8c-4159-ad7e-c1bce0958d25\"}\n[2026-05-07 12:29:14] 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\":\"d59f6990-3c72-4f1b-b5fa-4035b1be1dbb\",\"trace_id\":\"d502f355-1716-4272-9dc2-79f8c70cbbf4\"}\n[2026-05-07 12:29:14] 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\":\"d59f6990-3c72-4f1b-b5fa-4035b1be1dbb\",\"trace_id\":\"d502f355-1716-4272-9dc2-79f8c70cbbf4\"}\n[2026-05-07 12:29:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] 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\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3aa037e0-524c-4f99-a2fe-9537e1034e93\",\"trace_id\":\"c8f2c739-13e2-490c-9bce-6b940c4073cc\"}\n[2026-05-07 12:30:08] 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\":\"3aa037e0-524c-4f99-a2fe-9537e1034e93\",\"trace_id\":\"c8f2c739-13e2-490c-9bce-6b940c4073cc\"}\n[2026-05-07 12:30:12] local.NOTICE: Monitoring start {\"correlation_id\":\"33c6a1f2-608d-47f9-a340-97a81d930e48\",\"trace_id\":\"e07fb9c3-cf43-4caa-bb94-08be684e8e00\"}\n[2026-05-07 12:30:12] local.NOTICE: Monitoring end {\"correlation_id\":\"33c6a1f2-608d-47f9-a340-97a81d930e48\",\"trace_id\":\"e07fb9c3-cf43-4caa-bb94-08be684e8e00\"}\n[2026-05-07 12:30:14] 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\":\"8da3e1c6-c806-4120-a0d8-8d99e640c4d3\",\"trace_id\":\"4813fc1e-67c7-495d-bafe-1c0bddb52670\"}\n[2026-05-07 12:30:14] 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\":\"8da3e1c6-c806-4120-a0d8-8d99e640c4d3\",\"trace_id\":\"4813fc1e-67c7-495d-bafe-1c0bddb52670\"}\n[2026-05-07 12:30:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] 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\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:28:00, 2026-05-07 12:30:00] {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:28:00, 2026-05-07 12:30:00] {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"36d2678a-b734-46bd-990e-9968b2fb86d5\",\"trace_id\":\"0f9e0c4b-3fbb-400e-97a8-5b59b1c18d50\"}\n[2026-05-07 12:30:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"36d2678a-b734-46bd-990e-9968b2fb86d5\",\"trace_id\":\"0f9e0c4b-3fbb-400e-97a8-5b59b1c18d50\"}\n[2026-05-07 12:30:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3559116e-0397-44eb-a839-e397bbd4c3f4\",\"trace_id\":\"e765565d-25d2-45ab-b9a4-42ef784a60fc\"}\n[2026-05-07 12:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3559116e-0397-44eb-a839-e397bbd4c3f4\",\"trace_id\":\"e765565d-25d2-45ab-b9a4-42ef784a60fc\"}\n[2026-05-07 12:30:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:27] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:28] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: Running conference:monitor:start command for activities in (2026-05-07 12:20:00, 2026-05-07 12:25:00] {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: [conference:monitor:start] No activities found in (2026-05-07 12:20:00, 2026-05-07 12:25:00] {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"12:25\",\"to\":\"12:30\"} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"02:20\",\"to\":\"02:25\"} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:32] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3fce27e4-1b07-40fc-8a53-fbad08b093e2\",\"trace_id\":\"3b8bc557-6e3e-42bf-853b-b7b38c986086\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4b772eb2-f1f8-46d1-afe2-8c7e5bc14182\",\"trace_id\":\"a1b68813-a4cf-4767-8a9f-dde19264b31d\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3fce27e4-1b07-40fc-8a53-fbad08b093e2\",\"trace_id\":\"3b8bc557-6e3e-42bf-853b-b7b38c986086\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-05-07T12:32:37.470385Z\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4b772eb2-f1f8-46d1-afe2-8c7e5bc14182\",\"trace_id\":\"a1b68813-a4cf-4767-8a9f-dde19264b31d\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"101dc452-3092-4c93-b585-b80bc8b44698\",\"trace_id\":\"b65e71f7-dad2-4432-9357-c7f05e39ae8f\"}\n[2026-05-07 12:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"101dc452-3092-4c93-b585-b80bc8b44698\",\"trace_id\":\"b65e71f7-dad2-4432-9357-c7f05e39ae8f\"}\n[2026-05-07 12:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812319,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812320,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812321,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812322,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812323,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812324,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] 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\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"salesforce\",\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"salesforce\",\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.ALERT: [SyncActivity] Failed {\"import_id\":812319,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Social account for Salesforce cannot be found. Please login to Jiminny to connect.\",\"file\":\"/home/jiminny/app/Services/Crm/BaseService.php\",\"line\":646} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":812320,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":812321,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":812322,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6c34890d-27f6-452c-918a-d61dfb22dc78\",\"trace_id\":\"2c6a1876-1e2d-4c90-a235-129bfc1fda30\"}\n[2026-05-07 12:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6c34890d-27f6-452c-918a-d61dfb22dc78\",\"trace_id\":\"2c6a1876-1e2d-4c90-a235-129bfc1fda30\"}\n[2026-05-07 12:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":812323,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SyncActivity] Start {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [HubSpot] Search calls for period {\"from\":\"2026-05-07 12:14:00\",\"to\":\"2026-05-07 12:30:00\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:45] local.INFO: [SyncActivity] End {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2,\"memory_usage\":28981120,\"memory_real_usage\":67108864,\"pid\":26154} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:47] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a3e015c8-7c91-4457-bff4-de94482186fc\",\"trace_id\":\"2966aada-d948-4fec-8388-30f39c48648f\"}\n[2026-05-07 12:30:47] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a3e015c8-7c91-4457-bff4-de94482186fc\",\"trace_id\":\"2966aada-d948-4fec-8388-30f39c48648f\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal Auth] Requesting new client credentials token {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: [HubSpot Journal Auth] Successfully obtained new access token {\"expires_in\":1800,\"cached_for\":1500} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c40e82b8-a5fd-4588-a637-9bd875fc31d9\",\"trace_id\":\"038436d0-e786-4dc4-98f2-f235d5e96a8d\"}\n[2026-05-07 12:30:48] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c40e82b8-a5fd-4588-a637-9bd875fc31d9\",\"trace_id\":\"038436d0-e786-4dc4-98f2-f235d5e96a8d\"}\n[2026-05-07 12:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] starting. {\"playlists\":[]} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] finished. {\"normalizedPlaylists\":[],\"deletedPlaylists\":[]} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31:03] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31: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\":\"8970c115-8149-440f-9afd-94a6f7890ed9\",\"trace_id\":\"fef587d7-b0e8-48f0-85b9-b224f5bdbecc\"}\n[2026-05-07 12:31: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\":\"8970c115-8149-440f-9afd-94a6f7890ed9\",\"trace_id\":\"fef587d7-b0e8-48f0-85b9-b224f5bdbecc\"}\n[2026-05-07 12:31:08] local.NOTICE: Monitoring start {\"correlation_id\":\"8d299958-4bdf-4f50-a904-b3f63d3b6edf\",\"trace_id\":\"8ac0c53d-8ecc-4e29-a465-ff8af18b3565\"}\n[2026-05-07 12:31:08] local.NOTICE: Monitoring end {\"correlation_id\":\"8d299958-4bdf-4f50-a904-b3f63d3b6edf\",\"trace_id\":\"8ac0c53d-8ecc-4e29-a465-ff8af18b3565\"}\n[2026-05-07 12:31:10] 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\":\"d384715e-b7f3-4ebd-be32-f9c6c9683c6a\",\"trace_id\":\"7221d562-a73d-4a09-8f67-a096ad834b43\"}\n[2026-05-07 12:31:10] 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\":\"d384715e-b7f3-4ebd-be32-f9c6c9683c6a\",\"trace_id\":\"7221d562-a73d-4a09-8f67-a096ad834b43\"}\n[2026-05-07 12:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] 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\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"daa2a14e-13d8-4e03-ade8-5cf235aff8af\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"daa2a14e-13d8-4e03-ade8-5cf235aff8af\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":25525984,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] 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\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":68.42,\"usage\":25519152,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":25558232,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] 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.49,\"average_seconds_per_request\":0.49} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":495.28} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":516.14,\"usage\":25681432,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":25659360,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":21.43,\"usage\":25505056,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":25543424,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":15.8,\"usage\":25498888,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] 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\":218.1,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:32:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"99620b99-af26-48c8-9376-70139e107fd6\",\"trace_id\":\"7da3af1b-da2d-4610-b72c-3aabcf9b27a7\"}\n[2026-05-07 12:32:09] 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\":\"99620b99-af26-48c8-9376-70139e107fd6\",\"trace_id\":\"7da3af1b-da2d-4610-b72c-3aabcf9b27a7\"}\n[2026-05-07 12:32:12] local.NOTICE: Monitoring start {\"correlation_id\":\"0be593b8-0aea-40cb-8dc9-1e74bb44ede1\",\"trace_id\":\"1537b6d8-4150-4d77-b2c6-9cec45a0374b\"}\n[2026-05-07 12:32:12] local.NOTICE: Monitoring end {\"correlation_id\":\"0be593b8-0aea-40cb-8dc9-1e74bb44ede1\",\"trace_id\":\"1537b6d8-4150-4d77-b2c6-9cec45a0374b\"}\n[2026-05-07 12:32:17] 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\":\"32c250b0-69ea-4669-98ae-b965a9479044\",\"trace_id\":\"d5d62398-e8b5-4dc7-96f0-4d945c921e2d\"}\n[2026-05-07 12:32:17] 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\":\"32c250b0-69ea-4669-98ae-b965a9479044\",\"trace_id\":\"d5d62398-e8b5-4dc7-96f0-4d945c921e2d\"}\n[2026-05-07 12:32:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] 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\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:30:00, 2026-05-07 12:32:00] {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:30:00, 2026-05-07 12:32:00] {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] 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\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:27] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"7ae3582d-c6f0-4d3f-a055-768d0b080b2a\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}","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":"62","depth":4,"bounds":{"left":0.48969415,"top":0.17478053,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"bounds":{"left":0.50199467,"top":0.17478053,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"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.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1996073461129327348
|
8566805675520198815
|
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
Search History
Hubspot
New Line
Replace History
Replace
New Line
Previous Occurrence
Next Occurrence
Search All
Search Backward
Search Forward
Match сase
Words
Regex
?
Close
[2026-05-07 12:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:10] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"172d1ae8-b8cc-4804-bed9-e32d074e265c","trace_id":"4817cdef-8d3e-4914-8bee-feffb18efe1b"}
[2026-05-07 12:28:14] 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":"172d1ae8-b8cc-4804-bed9-e32d074e265c","trace_id":"4817cdef-8d3e-4914-8bee-feffb18efe1b"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1499,"provider":"hubspot","refreshToken":"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86","state":"connected"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.NOTICE: Monitoring start {"correlation_id":"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5","trace_id":"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72"}
[2026-05-07 12:28:18] local.NOTICE: Monitoring end {"correlation_id":"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5","trace_id":"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountService] Token refreshed {"socialAccountId":1499,"provider":"hubspot","state":"connected"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:19] local.INFO: [Hubspot] Failed to fetch opportunity {"crm_id":"374720564","reason":"[429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)
"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:19] local.ERROR: [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{"status":"error","message":"You have reached your ten_secondly_rolling limit.","errorType":"RATE_LIMIT","correlationId" (truncated...)
{"exception":"[object] (HubSpot\\Client\\Crm\\Deals\\ApiException(code: 429): [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)
at /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php(676): HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi->getByIdWithHttpInfo('374720564', 'hs_object_id,de...', 'companies,conta...', false, NULL)
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(212): HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi->getById('374720564', 'hs_object_id,de...', 'companies,conta...')
#2 /home/jiminny/app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php(130): Jiminny\\Services\\Crm\\Hubspot\\Client->getOpportunityById('374720564', Array)
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(351): Jiminny\\Services\\Crm\\Hubspot\\Service->syncOpportunity('374720564')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:20] 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":"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d","trace_id":"1ea62b83-9639-41e3-a523-26404c39fa80"}
[2026-05-07 12:28:20] 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":"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d","trace_id":"1ea62b83-9639-41e3-a523-26404c39fa80"}
[2026-05-07 12:28:24] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] 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":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:27] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"calendar:sync","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"3789f06a-4f0c-4b12-be80-d6a36e089d1b","trace_id":"6ce89147-1624-456d-9e75-f4948f2c5db8"}
[2026-05-07 12:28:30] local.NOTICE: Calendar sync start {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"3789f06a-4f0c-4b12-be80-d6a36e089d1b","trace_id":"6ce89147-1624-456d-9e75-f4948f2c5db8"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1393,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1393,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1393,"provider":"google","refreshToken":"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1387,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1387,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1387,"provider":"google","refreshToken":"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1348,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1348,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1348,"provider":"google","refreshToken":"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1361,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1361,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1361,"provider":"google","refreshToken":"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1310,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1310,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1310,"provider":"google","refreshToken":"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1333,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1333,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1333,"provider":"google","refreshToken":"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1368,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1368,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1368,"provider":"google","refreshToken":"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1365,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1365,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1365,"provider":"google","refreshToken":"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1364,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1364,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1364,"provider":"google","refreshToken":"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1370,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1370,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1370,"provider":"office","refreshToken":"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] 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: e890fdc1-dbe8-4a59-ae57-2af6bced3c00 Correlation ID: 57554cdf-16df-47f1-b0d9-5f0b8da37afe Timestamp: 2026-05-07 12:28:33Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:33Z\",\"trace_id\":\"e890fdc1-dbe8-4a59-ae57-2af6bced3c00\",\"correlation_id\":\"57554cdf-16df-47f1-b0d9-5f0b8da37afe\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1202,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1202,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1202,"provider":"office","refreshToken":"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] 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: 2a3e5b60-5770-46f2-aca6-7b0527363000 Correlation ID: 57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2 Timestamp: 2026-05-07 12:28:34Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:34Z\",\"trace_id\":\"2a3e5b60-5770-46f2-aca6-7b0527363000\",\"correlation_id\":\"57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: Calendar sync job dispatched {"calendar_id":501} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1300,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1300,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1300,"provider":"google","refreshToken":"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1409,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1409,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1409,"provider":"google","refreshToken":"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1352,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1352,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1352,"provider":"google","refreshToken":"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1296,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1296,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1296,"provider":"office","refreshToken":"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] 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":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"integration-app","crm_owner":1695,"team_id":3143} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] 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: 2e34e335-a76d-40e5-bc50-3861392e4c00 Correlation ID: 9db5b389-6165-4feb-8e6e-bedc369e1c87 Timestamp: 2026-05-07 12:28:35Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:35Z\",\"trace_id\":\"2e34e335-a76d-40e5-bc50-3861392e4c00\",\"correlation_id\":\"9db5b389-6165-4feb-8e6e-bedc369e1c87\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":391,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":391,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":391,"provider":"office","refreshToken":"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.INFO: [Google Calendar] Failed to watch channel for calendar {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.WARNING: [Calendar] Sync failed {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] 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: 6eba7173-b781-4e55-b1fb-1087ed023000 Correlation ID: 3086e346-c6eb-4f1c-8b1d-a477ce3821f4 Timestamp: 2026-05-07 12:28:36Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:36Z\",\"trace_id\":\"6eba7173-b781-4e55-b1fb-1087ed023000\",\"correlation_id\":\"3086e346-c6eb-4f1c-8b1d-a477ce3821f4\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1271,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1271,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1271,"provider":"office","refreshToken":"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1271,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided...
|
3641
|
NULL
|
NULL
|
NULL
|
|
3642
|
133
|
9
|
2026-05-07T12:32:32.505237+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778157152505_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/…/SyncCrmEntitiesTrait.php
|
1
|
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
Search History
Hubspot
New Line
Replace History
Replace
New Line
Previous Occurrence
Next Occurrence
Search All
Search Backward
Search Forward
Match сase
Words
Regex
?
Close
[2026-05-07 12:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:10] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"172d1ae8-b8cc-4804-bed9-e32d074e265c","trace_id":"4817cdef-8d3e-4914-8bee-feffb18efe1b"}
[2026-05-07 12:28:14] 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":"172d1ae8-b8cc-4804-bed9-e32d074e265c","trace_id":"4817cdef-8d3e-4914-8bee-feffb18efe1b"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1499,"provider":"hubspot","refreshToken":"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86","state":"connected"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.NOTICE: Monitoring start {"correlation_id":"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5","trace_id":"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72"}
[2026-05-07 12:28:18] local.NOTICE: Monitoring end {"correlation_id":"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5","trace_id":"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountService] Token refreshed {"socialAccountId":1499,"provider":"hubspot","state":"connected"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:19] local.INFO: [Hubspot] Failed to fetch opportunity {"crm_id":"374720564","reason":"[429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)
"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:19] local.ERROR: [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{"status":"error","message":"You have reached your ten_secondly_rolling limit.","errorType":"RATE_LIMIT","correlationId" (truncated...)
{"exception":"[object] (HubSpot\\Client\\Crm\\Deals\\ApiException(code: 429): [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)
at /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php(676): HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi->getByIdWithHttpInfo('374720564', 'hs_object_id,de...', 'companies,conta...', false, NULL)
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(212): HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi->getById('374720564', 'hs_object_id,de...', 'companies,conta...')
#2 /home/jiminny/app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php(130): Jiminny\\Services\\Crm\\Hubspot\\Client->getOpportunityById('374720564', Array)
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(351): Jiminny\\Services\\Crm\\Hubspot\\Service->syncOpportunity('374720564')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:20] 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":"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d","trace_id":"1ea62b83-9639-41e3-a523-26404c39fa80"}
[2026-05-07 12:28:20] 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":"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d","trace_id":"1ea62b83-9639-41e3-a523-26404c39fa80"}
[2026-05-07 12:28:24] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] 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":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:27] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"calendar:sync","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"3789f06a-4f0c-4b12-be80-d6a36e089d1b","trace_id":"6ce89147-1624-456d-9e75-f4948f2c5db8"}
[2026-05-07 12:28:30] local.NOTICE: Calendar sync start {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"3789f06a-4f0c-4b12-be80-d6a36e089d1b","trace_id":"6ce89147-1624-456d-9e75-f4948f2c5db8"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1393,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1393,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1393,"provider":"google","refreshToken":"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1387,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1387,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1387,"provider":"google","refreshToken":"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1348,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1348,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1348,"provider":"google","refreshToken":"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1361,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1361,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1361,"provider":"google","refreshToken":"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1310,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1310,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1310,"provider":"google","refreshToken":"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1333,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1333,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1333,"provider":"google","refreshToken":"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1368,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1368,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1368,"provider":"google","refreshToken":"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1365,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1365,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1365,"provider":"google","refreshToken":"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1364,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1364,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1364,"provider":"google","refreshToken":"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1370,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1370,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1370,"provider":"office","refreshToken":"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] 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: e890fdc1-dbe8-4a59-ae57-2af6bced3c00 Correlation ID: 57554cdf-16df-47f1-b0d9-5f0b8da37afe Timestamp: 2026-05-07 12:28:33Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:33Z\",\"trace_id\":\"e890fdc1-dbe8-4a59-ae57-2af6bced3c00\",\"correlation_id\":\"57554cdf-16df-47f1-b0d9-5f0b8da37afe\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1202,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1202,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1202,"provider":"office","refreshToken":"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] 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: 2a3e5b60-5770-46f2-aca6-7b0527363000 Correlation ID: 57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2 Timestamp: 2026-05-07 12:28:34Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:34Z\",\"trace_id\":\"2a3e5b60-5770-46f2-aca6-7b0527363000\",\"correlation_id\":\"57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: Calendar sync job dispatched {"calendar_id":501} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1300,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1300,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1300,"provider":"google","refreshToken":"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1409,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1409,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1409,"provider":"google","refreshToken":"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1352,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1352,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1352,"provider":"google","refreshToken":"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1296,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1296,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1296,"provider":"office","refreshToken":"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] 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":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"integration-app","crm_owner":1695,"team_id":3143} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] 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: 2e34e335-a76d-40e5-bc50-3861392e4c00 Correlation ID: 9db5b389-6165-4feb-8e6e-bedc369e1c87 Timestamp: 2026-05-07 12:28:35Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:35Z\",\"trace_id\":\"2e34e335-a76d-40e5-bc50-3861392e4c00\",\"correlation_id\":\"9db5b389-6165-4feb-8e6e-bedc369e1c87\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":391,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":391,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":391,"provider":"office","refreshToken":"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.INFO: [Google Calendar] Failed to watch channel for calendar {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.WARNING: [Calendar] Sync failed {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] 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: 6eba7173-b781-4e55-b1fb-1087ed023000 Correlation ID: 3086e346-c6eb-4f1c-8b1d-a477ce3821f4 Timestamp: 2026-05-07 12:28:36Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:36Z\",\"trace_id\":\"6eba7173-b781-4e55-b1fb-1087ed023000\",\"correlation_id\":\"3086e346-c6eb-4f1c-8b1d-a477ce3821f4\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1271,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1271,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1271,"provider":"office","refreshToken":"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1271,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided...
|
[{"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<br/>Some incoming commits are not fetched<br/>","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":"AXCheckBox","text":"Search History","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Hubspot","depth":4,"on_screen":true,"value":"Hubspot","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Backward","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Forward","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match сase","depth":4,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":4,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":4,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"?","depth":4,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 12:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:10] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"172d1ae8-b8cc-4804-bed9-e32d074e265c\",\"trace_id\":\"4817cdef-8d3e-4914-8bee-feffb18efe1b\"}\n[2026-05-07 12:28:14] 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\":\"172d1ae8-b8cc-4804-bed9-e32d074e265c\",\"trace_id\":\"4817cdef-8d3e-4914-8bee-feffb18efe1b\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"refreshToken\":\"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86\",\"state\":\"connected\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.NOTICE: Monitoring start {\"correlation_id\":\"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5\",\"trace_id\":\"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72\"}\n[2026-05-07 12:28:18] local.NOTICE: Monitoring end {\"correlation_id\":\"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5\",\"trace_id\":\"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"state\":\"connected\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:19] local.INFO: [Hubspot] Failed to fetch opportunity {\"crm_id\":\"374720564\",\"reason\":\"[429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your ten_secondly_rolling limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\" (truncated...)\n\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:19] local.ERROR: [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n {\"exception\":\"[object] (HubSpot\\\\Client\\\\Crm\\\\Deals\\\\ApiException(code: 429): [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your ten_secondly_rolling limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\" (truncated...)\n at /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php(676): HubSpot\\\\Client\\\\Crm\\\\Deals\\\\Api\\\\BasicApi->getByIdWithHttpInfo('374720564', 'hs_object_id,de...', 'companies,conta...', false, NULL)\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(212): HubSpot\\\\Client\\\\Crm\\\\Deals\\\\Api\\\\BasicApi->getById('374720564', 'hs_object_id,de...', 'companies,conta...')\n#2 /home/jiminny/app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php(130): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getOpportunityById('374720564', Array)\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(351): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->syncOpportunity('374720564')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:20] 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\":\"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d\",\"trace_id\":\"1ea62b83-9639-41e3-a523-26404c39fa80\"}\n[2026-05-07 12:28:20] 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\":\"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d\",\"trace_id\":\"1ea62b83-9639-41e3-a523-26404c39fa80\"}\n[2026-05-07 12:28:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] 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\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3789f06a-4f0c-4b12-be80-d6a36e089d1b\",\"trace_id\":\"6ce89147-1624-456d-9e75-f4948f2c5db8\"}\n[2026-05-07 12:28:30] local.NOTICE: Calendar sync start {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3789f06a-4f0c-4b12-be80-d6a36e089d1b\",\"trace_id\":\"6ce89147-1624-456d-9e75-f4948f2c5db8\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] 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: e890fdc1-dbe8-4a59-ae57-2af6bced3c00 Correlation ID: 57554cdf-16df-47f1-b0d9-5f0b8da37afe Timestamp: 2026-05-07 12:28:33Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:33Z\\\",\\\"trace_id\\\":\\\"e890fdc1-dbe8-4a59-ae57-2af6bced3c00\\\",\\\"correlation_id\\\":\\\"57554cdf-16df-47f1-b0d9-5f0b8da37afe\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] 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: 2a3e5b60-5770-46f2-aca6-7b0527363000 Correlation ID: 57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2 Timestamp: 2026-05-07 12:28:34Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:34Z\\\",\\\"trace_id\\\":\\\"2a3e5b60-5770-46f2-aca6-7b0527363000\\\",\\\"correlation_id\\\":\\\"57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] 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: 2e34e335-a76d-40e5-bc50-3861392e4c00 Correlation ID: 9db5b389-6165-4feb-8e6e-bedc369e1c87 Timestamp: 2026-05-07 12:28:35Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:35Z\\\",\\\"trace_id\\\":\\\"2e34e335-a76d-40e5-bc50-3861392e4c00\\\",\\\"correlation_id\\\":\\\"9db5b389-6165-4feb-8e6e-bedc369e1c87\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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: 6eba7173-b781-4e55-b1fb-1087ed023000 Correlation ID: 3086e346-c6eb-4f1c-8b1d-a477ce3821f4 Timestamp: 2026-05-07 12:28:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:36Z\\\",\\\"trace_id\\\":\\\"6eba7173-b781-4e55-b1fb-1087ed023000\\\",\\\"correlation_id\\\":\\\"3086e346-c6eb-4f1c-8b1d-a477ce3821f4\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] 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: b18cbf88-c6d0-4caa-9af9-d2dabb673500 Correlation ID: ead4f7c0-3077-42bb-84d0-c3b9a1432182 Timestamp: 2026-05-07 12:28:37Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:37Z\\\",\\\"trace_id\\\":\\\"b18cbf88-c6d0-4caa-9af9-d2dabb673500\\\",\\\"correlation_id\\\":\\\"ead4f7c0-3077-42bb-84d0-c3b9a1432182\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCHhwR3crxfEuMI8zGlf-bMYpCFtdxXvSJWTlnqQvu_jjoOrOYL2VG9rZwFHCERHxGfGEK3CmQX6x8MJG3ZbBXGuVIS6C7u-doY5maMRdsfnrHIAEMJd4Bs_WMfMH4tDJ8j9aul7DHDEJaP7w0PoPPpcoxu4nEk4vk-MolJBEgkSrayEewuBs5JVItUX9lUY2tA.yO2roNQ4Vdm6hBgoutuphGchuzbvsk7aqt5wHfcyeFQ\",\"last_sync\":\"2026-05-06 15:58:35\",\"dateMode\":\"daily\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:29:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"02cf8f7b-abd2-4e63-87bc-3e8fce928261\",\"trace_id\":\"6752601b-1c03-48ae-a590-323cc5ed601e\"}\n[2026-05-07 12:29:10] 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\":\"02cf8f7b-abd2-4e63-87bc-3e8fce928261\",\"trace_id\":\"6752601b-1c03-48ae-a590-323cc5ed601e\"}\n[2026-05-07 12:29:12] local.NOTICE: Monitoring start {\"correlation_id\":\"d3765bc2-52fa-4bc4-9c94-52822e08f7a5\",\"trace_id\":\"0299fb07-ac8c-4159-ad7e-c1bce0958d25\"}\n[2026-05-07 12:29:12] local.NOTICE: Monitoring end {\"correlation_id\":\"d3765bc2-52fa-4bc4-9c94-52822e08f7a5\",\"trace_id\":\"0299fb07-ac8c-4159-ad7e-c1bce0958d25\"}\n[2026-05-07 12:29:14] 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\":\"d59f6990-3c72-4f1b-b5fa-4035b1be1dbb\",\"trace_id\":\"d502f355-1716-4272-9dc2-79f8c70cbbf4\"}\n[2026-05-07 12:29:14] 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\":\"d59f6990-3c72-4f1b-b5fa-4035b1be1dbb\",\"trace_id\":\"d502f355-1716-4272-9dc2-79f8c70cbbf4\"}\n[2026-05-07 12:29:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] 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\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3aa037e0-524c-4f99-a2fe-9537e1034e93\",\"trace_id\":\"c8f2c739-13e2-490c-9bce-6b940c4073cc\"}\n[2026-05-07 12:30:08] 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\":\"3aa037e0-524c-4f99-a2fe-9537e1034e93\",\"trace_id\":\"c8f2c739-13e2-490c-9bce-6b940c4073cc\"}\n[2026-05-07 12:30:12] local.NOTICE: Monitoring start {\"correlation_id\":\"33c6a1f2-608d-47f9-a340-97a81d930e48\",\"trace_id\":\"e07fb9c3-cf43-4caa-bb94-08be684e8e00\"}\n[2026-05-07 12:30:12] local.NOTICE: Monitoring end {\"correlation_id\":\"33c6a1f2-608d-47f9-a340-97a81d930e48\",\"trace_id\":\"e07fb9c3-cf43-4caa-bb94-08be684e8e00\"}\n[2026-05-07 12:30:14] 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\":\"8da3e1c6-c806-4120-a0d8-8d99e640c4d3\",\"trace_id\":\"4813fc1e-67c7-495d-bafe-1c0bddb52670\"}\n[2026-05-07 12:30:14] 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\":\"8da3e1c6-c806-4120-a0d8-8d99e640c4d3\",\"trace_id\":\"4813fc1e-67c7-495d-bafe-1c0bddb52670\"}\n[2026-05-07 12:30:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] 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\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:28:00, 2026-05-07 12:30:00] {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:28:00, 2026-05-07 12:30:00] {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"36d2678a-b734-46bd-990e-9968b2fb86d5\",\"trace_id\":\"0f9e0c4b-3fbb-400e-97a8-5b59b1c18d50\"}\n[2026-05-07 12:30:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"36d2678a-b734-46bd-990e-9968b2fb86d5\",\"trace_id\":\"0f9e0c4b-3fbb-400e-97a8-5b59b1c18d50\"}\n[2026-05-07 12:30:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3559116e-0397-44eb-a839-e397bbd4c3f4\",\"trace_id\":\"e765565d-25d2-45ab-b9a4-42ef784a60fc\"}\n[2026-05-07 12:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3559116e-0397-44eb-a839-e397bbd4c3f4\",\"trace_id\":\"e765565d-25d2-45ab-b9a4-42ef784a60fc\"}\n[2026-05-07 12:30:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:27] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:28] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: Running conference:monitor:start command for activities in (2026-05-07 12:20:00, 2026-05-07 12:25:00] {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: [conference:monitor:start] No activities found in (2026-05-07 12:20:00, 2026-05-07 12:25:00] {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"12:25\",\"to\":\"12:30\"} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"02:20\",\"to\":\"02:25\"} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:32] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3fce27e4-1b07-40fc-8a53-fbad08b093e2\",\"trace_id\":\"3b8bc557-6e3e-42bf-853b-b7b38c986086\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4b772eb2-f1f8-46d1-afe2-8c7e5bc14182\",\"trace_id\":\"a1b68813-a4cf-4767-8a9f-dde19264b31d\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3fce27e4-1b07-40fc-8a53-fbad08b093e2\",\"trace_id\":\"3b8bc557-6e3e-42bf-853b-b7b38c986086\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-05-07T12:32:37.470385Z\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4b772eb2-f1f8-46d1-afe2-8c7e5bc14182\",\"trace_id\":\"a1b68813-a4cf-4767-8a9f-dde19264b31d\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"101dc452-3092-4c93-b585-b80bc8b44698\",\"trace_id\":\"b65e71f7-dad2-4432-9357-c7f05e39ae8f\"}\n[2026-05-07 12:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"101dc452-3092-4c93-b585-b80bc8b44698\",\"trace_id\":\"b65e71f7-dad2-4432-9357-c7f05e39ae8f\"}\n[2026-05-07 12:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812319,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812320,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812321,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812322,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812323,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812324,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] 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\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"salesforce\",\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"salesforce\",\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.ALERT: [SyncActivity] Failed {\"import_id\":812319,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Social account for Salesforce cannot be found. Please login to Jiminny to connect.\",\"file\":\"/home/jiminny/app/Services/Crm/BaseService.php\",\"line\":646} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":812320,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":812321,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":812322,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6c34890d-27f6-452c-918a-d61dfb22dc78\",\"trace_id\":\"2c6a1876-1e2d-4c90-a235-129bfc1fda30\"}\n[2026-05-07 12:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6c34890d-27f6-452c-918a-d61dfb22dc78\",\"trace_id\":\"2c6a1876-1e2d-4c90-a235-129bfc1fda30\"}\n[2026-05-07 12:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":812323,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SyncActivity] Start {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [HubSpot] Search calls for period {\"from\":\"2026-05-07 12:14:00\",\"to\":\"2026-05-07 12:30:00\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:45] local.INFO: [SyncActivity] End {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2,\"memory_usage\":28981120,\"memory_real_usage\":67108864,\"pid\":26154} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:47] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a3e015c8-7c91-4457-bff4-de94482186fc\",\"trace_id\":\"2966aada-d948-4fec-8388-30f39c48648f\"}\n[2026-05-07 12:30:47] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a3e015c8-7c91-4457-bff4-de94482186fc\",\"trace_id\":\"2966aada-d948-4fec-8388-30f39c48648f\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal Auth] Requesting new client credentials token {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: [HubSpot Journal Auth] Successfully obtained new access token {\"expires_in\":1800,\"cached_for\":1500} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c40e82b8-a5fd-4588-a637-9bd875fc31d9\",\"trace_id\":\"038436d0-e786-4dc4-98f2-f235d5e96a8d\"}\n[2026-05-07 12:30:48] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c40e82b8-a5fd-4588-a637-9bd875fc31d9\",\"trace_id\":\"038436d0-e786-4dc4-98f2-f235d5e96a8d\"}\n[2026-05-07 12:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] starting. {\"playlists\":[]} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] finished. {\"normalizedPlaylists\":[],\"deletedPlaylists\":[]} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31:03] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31: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\":\"8970c115-8149-440f-9afd-94a6f7890ed9\",\"trace_id\":\"fef587d7-b0e8-48f0-85b9-b224f5bdbecc\"}\n[2026-05-07 12:31: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\":\"8970c115-8149-440f-9afd-94a6f7890ed9\",\"trace_id\":\"fef587d7-b0e8-48f0-85b9-b224f5bdbecc\"}\n[2026-05-07 12:31:08] local.NOTICE: Monitoring start {\"correlation_id\":\"8d299958-4bdf-4f50-a904-b3f63d3b6edf\",\"trace_id\":\"8ac0c53d-8ecc-4e29-a465-ff8af18b3565\"}\n[2026-05-07 12:31:08] local.NOTICE: Monitoring end {\"correlation_id\":\"8d299958-4bdf-4f50-a904-b3f63d3b6edf\",\"trace_id\":\"8ac0c53d-8ecc-4e29-a465-ff8af18b3565\"}\n[2026-05-07 12:31:10] 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\":\"d384715e-b7f3-4ebd-be32-f9c6c9683c6a\",\"trace_id\":\"7221d562-a73d-4a09-8f67-a096ad834b43\"}\n[2026-05-07 12:31:10] 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\":\"d384715e-b7f3-4ebd-be32-f9c6c9683c6a\",\"trace_id\":\"7221d562-a73d-4a09-8f67-a096ad834b43\"}\n[2026-05-07 12:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] 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\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"daa2a14e-13d8-4e03-ade8-5cf235aff8af\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"daa2a14e-13d8-4e03-ade8-5cf235aff8af\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":25525984,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] 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\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":68.42,\"usage\":25519152,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":25558232,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] 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.49,\"average_seconds_per_request\":0.49} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":495.28} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":516.14,\"usage\":25681432,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":25659360,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":21.43,\"usage\":25505056,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":25543424,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":15.8,\"usage\":25498888,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] 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\":218.1,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:32:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"99620b99-af26-48c8-9376-70139e107fd6\",\"trace_id\":\"7da3af1b-da2d-4610-b72c-3aabcf9b27a7\"}\n[2026-05-07 12:32:09] 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\":\"99620b99-af26-48c8-9376-70139e107fd6\",\"trace_id\":\"7da3af1b-da2d-4610-b72c-3aabcf9b27a7\"}\n[2026-05-07 12:32:12] local.NOTICE: Monitoring start {\"correlation_id\":\"0be593b8-0aea-40cb-8dc9-1e74bb44ede1\",\"trace_id\":\"1537b6d8-4150-4d77-b2c6-9cec45a0374b\"}\n[2026-05-07 12:32:12] local.NOTICE: Monitoring end {\"correlation_id\":\"0be593b8-0aea-40cb-8dc9-1e74bb44ede1\",\"trace_id\":\"1537b6d8-4150-4d77-b2c6-9cec45a0374b\"}\n[2026-05-07 12:32:17] 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\":\"32c250b0-69ea-4669-98ae-b965a9479044\",\"trace_id\":\"d5d62398-e8b5-4dc7-96f0-4d945c921e2d\"}\n[2026-05-07 12:32:17] 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\":\"32c250b0-69ea-4669-98ae-b965a9479044\",\"trace_id\":\"d5d62398-e8b5-4dc7-96f0-4d945c921e2d\"}\n[2026-05-07 12:32:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] 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\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:30:00, 2026-05-07 12:32:00] {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:30:00, 2026-05-07 12:32:00] {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] 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\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:27] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"7ae3582d-c6f0-4d3f-a055-768d0b080b2a\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}","depth":4,"on_screen":true,"value":"[2026-05-07 12:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:10] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d4ce87a9-6ca5-4efe-99b3-4220893270d0\",\"trace_id\":\"724e8238-2ed5-4089-9509-9c7c12a3c373\"}\n[2026-05-07 12:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"172d1ae8-b8cc-4804-bed9-e32d074e265c\",\"trace_id\":\"4817cdef-8d3e-4914-8bee-feffb18efe1b\"}\n[2026-05-07 12:28:14] 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\":\"172d1ae8-b8cc-4804-bed9-e32d074e265c\",\"trace_id\":\"4817cdef-8d3e-4914-8bee-feffb18efe1b\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"refreshToken\":\"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86\",\"state\":\"connected\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.NOTICE: Monitoring start {\"correlation_id\":\"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5\",\"trace_id\":\"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72\"}\n[2026-05-07 12:28:18] local.NOTICE: Monitoring end {\"correlation_id\":\"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5\",\"trace_id\":\"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"state\":\"connected\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:18] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:19] local.INFO: [Hubspot] Failed to fetch opportunity {\"crm_id\":\"374720564\",\"reason\":\"[429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your ten_secondly_rolling limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\" (truncated...)\n\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:19] local.ERROR: [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n {\"exception\":\"[object] (HubSpot\\\\Client\\\\Crm\\\\Deals\\\\ApiException(code: 429): [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your ten_secondly_rolling limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\" (truncated...)\n at /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php(676): HubSpot\\\\Client\\\\Crm\\\\Deals\\\\Api\\\\BasicApi->getByIdWithHttpInfo('374720564', 'hs_object_id,de...', 'companies,conta...', false, NULL)\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(212): HubSpot\\\\Client\\\\Crm\\\\Deals\\\\Api\\\\BasicApi->getById('374720564', 'hs_object_id,de...', 'companies,conta...')\n#2 /home/jiminny/app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php(130): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getOpportunityById('374720564', Array)\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(351): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->syncOpportunity('374720564')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\"} {\"correlation_id\":\"273355f2-6315-4b20-bc9a-c26c681d6344\",\"trace_id\":\"5ea79a26-c838-48e8-9913-93ad508146a6\"}\n[2026-05-07 12:28:20] 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\":\"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d\",\"trace_id\":\"1ea62b83-9639-41e3-a523-26404c39fa80\"}\n[2026-05-07 12:28:20] 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\":\"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d\",\"trace_id\":\"1ea62b83-9639-41e3-a523-26404c39fa80\"}\n[2026-05-07 12:28:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:24] 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\":\"d191d5c6-133f-43e1-8ed4-d285d0c768b8\",\"trace_id\":\"67798138-bdb7-4bd7-8b32-377663219a88\"}\n[2026-05-07 12:28:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a4e5e18f-9f8c-4196-ace0-bf664d4827d9\",\"trace_id\":\"d75cb10b-4c76-4c00-84d4-26efeb738e17\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3789f06a-4f0c-4b12-be80-d6a36e089d1b\",\"trace_id\":\"6ce89147-1624-456d-9e75-f4948f2c5db8\"}\n[2026-05-07 12:28:30] local.NOTICE: Calendar sync start {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3789f06a-4f0c-4b12-be80-d6a36e089d1b\",\"trace_id\":\"6ce89147-1624-456d-9e75-f4948f2c5db8\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] 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: e890fdc1-dbe8-4a59-ae57-2af6bced3c00 Correlation ID: 57554cdf-16df-47f1-b0d9-5f0b8da37afe Timestamp: 2026-05-07 12:28:33Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:33Z\\\",\\\"trace_id\\\":\\\"e890fdc1-dbe8-4a59-ae57-2af6bced3c00\\\",\\\"correlation_id\\\":\\\"57554cdf-16df-47f1-b0d9-5f0b8da37afe\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] 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: 2a3e5b60-5770-46f2-aca6-7b0527363000 Correlation ID: 57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2 Timestamp: 2026-05-07 12:28:34Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:34Z\\\",\\\"trace_id\\\":\\\"2a3e5b60-5770-46f2-aca6-7b0527363000\\\",\\\"correlation_id\\\":\\\"57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] 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: 2e34e335-a76d-40e5-bc50-3861392e4c00 Correlation ID: 9db5b389-6165-4feb-8e6e-bedc369e1c87 Timestamp: 2026-05-07 12:28:35Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:35Z\\\",\\\"trace_id\\\":\\\"2e34e335-a76d-40e5-bc50-3861392e4c00\\\",\\\"correlation_id\\\":\\\"9db5b389-6165-4feb-8e6e-bedc369e1c87\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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\":\"3015918c-9edf-487d-b1d0-97c9d00ea6b1\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] 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: 6eba7173-b781-4e55-b1fb-1087ed023000 Correlation ID: 3086e346-c6eb-4f1c-8b1d-a477ce3821f4 Timestamp: 2026-05-07 12:28:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:36Z\\\",\\\"trace_id\\\":\\\"6eba7173-b781-4e55-b1fb-1087ed023000\\\",\\\"correlation_id\\\":\\\"3086e346-c6eb-4f1c-8b1d-a477ce3821f4\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] 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: b18cbf88-c6d0-4caa-9af9-d2dabb673500 Correlation ID: ead4f7c0-3077-42bb-84d0-c3b9a1432182 Timestamp: 2026-05-07 12:28:37Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-07 12:28:37Z\\\",\\\"trace_id\\\":\\\"b18cbf88-c6d0-4caa-9af9-d2dabb673500\\\",\\\"correlation_id\\\":\\\"ead4f7c0-3077-42bb-84d0-c3b9a1432182\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:38] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d543a089-fd32-4522-9ffe-1494e562b741\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:40] 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\":\"d023e7ab-3a56-433c-841b-49c58c0cb46d\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCHhwR3crxfEuMI8zGlf-bMYpCFtdxXvSJWTlnqQvu_jjoOrOYL2VG9rZwFHCERHxGfGEK3CmQX6x8MJG3ZbBXGuVIS6C7u-doY5maMRdsfnrHIAEMJd4Bs_WMfMH4tDJ8j9aul7DHDEJaP7w0PoPPpcoxu4nEk4vk-MolJBEgkSrayEewuBs5JVItUX9lUY2tA.yO2roNQ4Vdm6hBgoutuphGchuzbvsk7aqt5wHfcyeFQ\",\"last_sync\":\"2026-05-06 15:58:35\",\"dateMode\":\"daily\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:28:41] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"9ccaf562-7a14-4f4b-8625-81e2bf970988\",\"trace_id\":\"564a362b-7ddb-49f0-a33a-b807b0440231\"}\n[2026-05-07 12:29:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d6b5d8c-5396-45af-b4f1-348978acd054\",\"trace_id\":\"031091b8-40ce-4d57-a4e9-bc4d37034ae0\"}\n[2026-05-07 12:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"02cf8f7b-abd2-4e63-87bc-3e8fce928261\",\"trace_id\":\"6752601b-1c03-48ae-a590-323cc5ed601e\"}\n[2026-05-07 12:29:10] 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\":\"02cf8f7b-abd2-4e63-87bc-3e8fce928261\",\"trace_id\":\"6752601b-1c03-48ae-a590-323cc5ed601e\"}\n[2026-05-07 12:29:12] local.NOTICE: Monitoring start {\"correlation_id\":\"d3765bc2-52fa-4bc4-9c94-52822e08f7a5\",\"trace_id\":\"0299fb07-ac8c-4159-ad7e-c1bce0958d25\"}\n[2026-05-07 12:29:12] local.NOTICE: Monitoring end {\"correlation_id\":\"d3765bc2-52fa-4bc4-9c94-52822e08f7a5\",\"trace_id\":\"0299fb07-ac8c-4159-ad7e-c1bce0958d25\"}\n[2026-05-07 12:29:14] 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\":\"d59f6990-3c72-4f1b-b5fa-4035b1be1dbb\",\"trace_id\":\"d502f355-1716-4272-9dc2-79f8c70cbbf4\"}\n[2026-05-07 12:29:14] 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\":\"d59f6990-3c72-4f1b-b5fa-4035b1be1dbb\",\"trace_id\":\"d502f355-1716-4272-9dc2-79f8c70cbbf4\"}\n[2026-05-07 12:29:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:29:16] 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\":\"6e7dd39a-53bc-4359-a50f-7519f217bb9a\",\"trace_id\":\"eef97a11-0a3b-4911-be85-f37bbdc01d15\"}\n[2026-05-07 12:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e4a1110b-dccf-46a7-b349-539201c9c258\",\"trace_id\":\"05679e00-ab34-47d6-af74-01a412a269b5\"}\n[2026-05-07 12:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3aa037e0-524c-4f99-a2fe-9537e1034e93\",\"trace_id\":\"c8f2c739-13e2-490c-9bce-6b940c4073cc\"}\n[2026-05-07 12:30:08] 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\":\"3aa037e0-524c-4f99-a2fe-9537e1034e93\",\"trace_id\":\"c8f2c739-13e2-490c-9bce-6b940c4073cc\"}\n[2026-05-07 12:30:12] local.NOTICE: Monitoring start {\"correlation_id\":\"33c6a1f2-608d-47f9-a340-97a81d930e48\",\"trace_id\":\"e07fb9c3-cf43-4caa-bb94-08be684e8e00\"}\n[2026-05-07 12:30:12] local.NOTICE: Monitoring end {\"correlation_id\":\"33c6a1f2-608d-47f9-a340-97a81d930e48\",\"trace_id\":\"e07fb9c3-cf43-4caa-bb94-08be684e8e00\"}\n[2026-05-07 12:30:14] 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\":\"8da3e1c6-c806-4120-a0d8-8d99e640c4d3\",\"trace_id\":\"4813fc1e-67c7-495d-bafe-1c0bddb52670\"}\n[2026-05-07 12:30:14] 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\":\"8da3e1c6-c806-4120-a0d8-8d99e640c4d3\",\"trace_id\":\"4813fc1e-67c7-495d-bafe-1c0bddb52670\"}\n[2026-05-07 12:30:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:16] 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\":\"a72f5370-11f9-4960-86c6-f91c0f8f3c17\",\"trace_id\":\"c6335d80-1601-41ed-8cbe-bc4a32bca434\"}\n[2026-05-07 12:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:28:00, 2026-05-07 12:30:00] {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:28:00, 2026-05-07 12:30:00] {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8aae162c-20b5-41f5-9b9a-cc6b832e8802\",\"trace_id\":\"0c29315d-717e-40e7-a21e-4ee0f9ec5ca0\"}\n[2026-05-07 12:30:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"36d2678a-b734-46bd-990e-9968b2fb86d5\",\"trace_id\":\"0f9e0c4b-3fbb-400e-97a8-5b59b1c18d50\"}\n[2026-05-07 12:30:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"36d2678a-b734-46bd-990e-9968b2fb86d5\",\"trace_id\":\"0f9e0c4b-3fbb-400e-97a8-5b59b1c18d50\"}\n[2026-05-07 12:30:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3559116e-0397-44eb-a839-e397bbd4c3f4\",\"trace_id\":\"e765565d-25d2-45ab-b9a4-42ef784a60fc\"}\n[2026-05-07 12:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3559116e-0397-44eb-a839-e397bbd4c3f4\",\"trace_id\":\"e765565d-25d2-45ab-b9a4-42ef784a60fc\"}\n[2026-05-07 12:30:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:27] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7756bdce-9659-4c40-885e-6cb4097a770a\",\"trace_id\":\"d7767470-3342-4939-845d-800b6e7e0bc9\"}\n[2026-05-07 12:30:28] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: Running conference:monitor:start command for activities in (2026-05-07 12:20:00, 2026-05-07 12:25:00] {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: [conference:monitor:start] No activities found in (2026-05-07 12:20:00, 2026-05-07 12:25:00] {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:28] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3aef2e59-302a-485b-b9ec-20478fd20cf5\",\"trace_id\":\"1dae0aa8-c6fd-4fca-84b2-2bb76ef6721b\"}\n[2026-05-07 12:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"12:25\",\"to\":\"12:30\"} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"02:20\",\"to\":\"02:25\"} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"46c43768-14e5-4886-b06b-de9c245deaf6\",\"trace_id\":\"4a12fadf-dd6f-4277-bac7-57d6789cbf99\"}\n[2026-05-07 12:30:32] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:32] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] 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\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:33] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"b5827753-349d-4c3e-8315-6edd21487988\",\"trace_id\":\"8f1a0949-4cda-47fd-b4d6-602eb0a03249\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3fce27e4-1b07-40fc-8a53-fbad08b093e2\",\"trace_id\":\"3b8bc557-6e3e-42bf-853b-b7b38c986086\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4b772eb2-f1f8-46d1-afe2-8c7e5bc14182\",\"trace_id\":\"a1b68813-a4cf-4767-8a9f-dde19264b31d\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3fce27e4-1b07-40fc-8a53-fbad08b093e2\",\"trace_id\":\"3b8bc557-6e3e-42bf-853b-b7b38c986086\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-05-07T12:32:37.470385Z\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4b772eb2-f1f8-46d1-afe2-8c7e5bc14182\",\"trace_id\":\"a1b68813-a4cf-4767-8a9f-dde19264b31d\"}\n[2026-05-07 12:30:37] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"101dc452-3092-4c93-b585-b80bc8b44698\",\"trace_id\":\"b65e71f7-dad2-4432-9357-c7f05e39ae8f\"}\n[2026-05-07 12:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"101dc452-3092-4c93-b585-b80bc8b44698\",\"trace_id\":\"b65e71f7-dad2-4432-9357-c7f05e39ae8f\"}\n[2026-05-07 12:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812319,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812320,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812321,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812322,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812323,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: Dispatching activity sync job {\"import_id\":812324,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] 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\":\"5a5ef5ba-0234-42c4-baae-827103cebea9\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:42] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"salesforce\",\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"salesforce\",\"team_id\":1} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.ALERT: [SyncActivity] Failed {\"import_id\":812319,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Social account for Salesforce cannot be found. Please login to Jiminny to connect.\",\"file\":\"/home/jiminny/app/Services/Crm/BaseService.php\",\"line\":646} {\"correlation_id\":\"f51bdd5f-03c8-4af6-831b-086c01094688\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":812320,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"d4e15c5d-717b-4c33-a888-1f98bc199608\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":812321,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"a420fb54-7bc6-4eb3-afc4-c88d457ac2b7\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":812322,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"3330dbba-b9ee-45de-8c39-b280897cc933\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1173,\"provider\":\"salesforce\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6c34890d-27f6-452c-918a-d61dfb22dc78\",\"trace_id\":\"2c6a1876-1e2d-4c90-a235-129bfc1fda30\"}\n[2026-05-07 12:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6c34890d-27f6-452c-918a-d61dfb22dc78\",\"trace_id\":\"2c6a1876-1e2d-4c90-a235-129bfc1fda30\"}\n[2026-05-07 12:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":812323,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"Activity Provider account not connected.\",\"file\":\"/home/jiminny/app/Services/Activity/ActivityProviderService.php\",\"line\":174} {\"correlation_id\":\"79f1a8bc-6445-4a65-a7e4-a4a2b273f5c4\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [SyncActivity] Start {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:44] local.INFO: [HubSpot] Search calls for period {\"from\":\"2026-05-07 12:14:00\",\"to\":\"2026-05-07 12:30:00\"} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:45] local.INFO: [SyncActivity] End {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":812324,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2,\"memory_usage\":28981120,\"memory_real_usage\":67108864,\"pid\":26154} {\"correlation_id\":\"aebf6b71-0ddc-409f-8ec6-933f9e06059c\",\"trace_id\":\"a71be838-d91c-4f4c-94e8-e34d38d6c0eb\"}\n[2026-05-07 12:30:47] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a3e015c8-7c91-4457-bff4-de94482186fc\",\"trace_id\":\"2966aada-d948-4fec-8388-30f39c48648f\"}\n[2026-05-07 12:30:47] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a3e015c8-7c91-4457-bff4-de94482186fc\",\"trace_id\":\"2966aada-d948-4fec-8388-30f39c48648f\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:47] local.INFO: [HubSpot Journal Auth] Requesting new client credentials token {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: [HubSpot Journal Auth] Successfully obtained new access token {\"expires_in\":1800,\"cached_for\":1500} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:30:48] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c40e82b8-a5fd-4588-a637-9bd875fc31d9\",\"trace_id\":\"038436d0-e786-4dc4-98f2-f235d5e96a8d\"}\n[2026-05-07 12:30:48] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c40e82b8-a5fd-4588-a637-9bd875fc31d9\",\"trace_id\":\"038436d0-e786-4dc4-98f2-f235d5e96a8d\"}\n[2026-05-07 12:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] starting. {\"playlists\":[]} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] finished. {\"normalizedPlaylists\":[],\"deletedPlaylists\":[]} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5a789fcf-bcf3-433d-8b99-9a04acf54e47\",\"trace_id\":\"b95235c3-8ded-488f-bb53-fe81a3bbe5a1\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31:03] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"15703b0e-3148-4521-97b0-119f62d4bf9a\",\"trace_id\":\"92237a7f-8aae-4d5d-9f0d-b21fb9f33c04\"}\n[2026-05-07 12:31: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\":\"8970c115-8149-440f-9afd-94a6f7890ed9\",\"trace_id\":\"fef587d7-b0e8-48f0-85b9-b224f5bdbecc\"}\n[2026-05-07 12:31: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\":\"8970c115-8149-440f-9afd-94a6f7890ed9\",\"trace_id\":\"fef587d7-b0e8-48f0-85b9-b224f5bdbecc\"}\n[2026-05-07 12:31:08] local.NOTICE: Monitoring start {\"correlation_id\":\"8d299958-4bdf-4f50-a904-b3f63d3b6edf\",\"trace_id\":\"8ac0c53d-8ecc-4e29-a465-ff8af18b3565\"}\n[2026-05-07 12:31:08] local.NOTICE: Monitoring end {\"correlation_id\":\"8d299958-4bdf-4f50-a904-b3f63d3b6edf\",\"trace_id\":\"8ac0c53d-8ecc-4e29-a465-ff8af18b3565\"}\n[2026-05-07 12:31:10] 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\":\"d384715e-b7f3-4ebd-be32-f9c6c9683c6a\",\"trace_id\":\"7221d562-a73d-4a09-8f67-a096ad834b43\"}\n[2026-05-07 12:31:10] 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\":\"d384715e-b7f3-4ebd-be32-f9c6c9683c6a\",\"trace_id\":\"7221d562-a73d-4a09-8f67-a096ad834b43\"}\n[2026-05-07 12:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:12] 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\":\"6b5409ea-62f5-44c3-b853-845c2b06c164\",\"trace_id\":\"3aef745f-0a16-4db4-92d7-b4c04316fcd4\"}\n[2026-05-07 12:31:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"daa2a14e-13d8-4e03-ade8-5cf235aff8af\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"daa2a14e-13d8-4e03-ade8-5cf235aff8af\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":25525984,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] 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\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":68.42,\"usage\":25519152,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"ceb90517-afd6-4f70-994b-f791459cbe9f\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":25558232,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:14] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] 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.49,\"average_seconds_per_request\":0.49} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":495.28} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":516.14,\"usage\":25681432,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"253d7263-dfb3-42f8-b025-93453379f3ac\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":25659360,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":21.43,\"usage\":25505056,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"79c64d03-4641-4366-9691-5b24a4e860b3\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":25543424,\"real_usage\":65011712,\"pid\":26151} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:15] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":15.8,\"usage\":25498888,\"real_usage\":65011712,\"pid\":26151,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"69f05987-da80-4ff2-8ace-9b4bffb3b7f7\",\"trace_id\":\"296f7872-e07a-4834-87f8-0c4d5594054d\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] 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\":218.1,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:31:33] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"bee413d5-e5f8-4384-b691-e30383fcd178\",\"trace_id\":\"0da49d98-27ef-441d-8fd8-465c8dc6d31c\"}\n[2026-05-07 12:32:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0e01e875-d2c2-41d3-956d-223b1e90dd29\",\"trace_id\":\"bbf4347e-5969-439e-a725-467084a07e73\"}\n[2026-05-07 12:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"99620b99-af26-48c8-9376-70139e107fd6\",\"trace_id\":\"7da3af1b-da2d-4610-b72c-3aabcf9b27a7\"}\n[2026-05-07 12:32:09] 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\":\"99620b99-af26-48c8-9376-70139e107fd6\",\"trace_id\":\"7da3af1b-da2d-4610-b72c-3aabcf9b27a7\"}\n[2026-05-07 12:32:12] local.NOTICE: Monitoring start {\"correlation_id\":\"0be593b8-0aea-40cb-8dc9-1e74bb44ede1\",\"trace_id\":\"1537b6d8-4150-4d77-b2c6-9cec45a0374b\"}\n[2026-05-07 12:32:12] local.NOTICE: Monitoring end {\"correlation_id\":\"0be593b8-0aea-40cb-8dc9-1e74bb44ede1\",\"trace_id\":\"1537b6d8-4150-4d77-b2c6-9cec45a0374b\"}\n[2026-05-07 12:32:17] 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\":\"32c250b0-69ea-4669-98ae-b965a9479044\",\"trace_id\":\"d5d62398-e8b5-4dc7-96f0-4d945c921e2d\"}\n[2026-05-07 12:32:17] 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\":\"32c250b0-69ea-4669-98ae-b965a9479044\",\"trace_id\":\"d5d62398-e8b5-4dc7-96f0-4d945c921e2d\"}\n[2026-05-07 12:32:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:19] 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\":\"edb12a01-c838-40ef-951d-413e5028f245\",\"trace_id\":\"00c83a82-64a6-40f5-a968-cdad57cd710a\"}\n[2026-05-07 12:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:30:00, 2026-05-07 12:32:00] {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:30:00, 2026-05-07 12:32:00] {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6749c715-7af6-4627-9ae9-566e158f698b\",\"trace_id\":\"76a70eba-ce35-4687-8705-48f62f076201\"}\n[2026-05-07 12:32:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:25] 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\":\"3fa722f4-e215-4794-a480-060a0542c380\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}\n[2026-05-07 12:32:27] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"7ae3582d-c6f0-4d3f-a055-768d0b080b2a\",\"trace_id\":\"8479ed65-dc02-43c7-908c-57d9365ba83e\"}","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":"62","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"32","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 Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1996073461129327348
|
8566805675520198815
|
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
Search History
Hubspot
New Line
Replace History
Replace
New Line
Previous Occurrence
Next Occurrence
Search All
Search Backward
Search Forward
Match сase
Words
Regex
?
Close
[2026-05-07 12:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:10] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"d4ce87a9-6ca5-4efe-99b3-4220893270d0","trace_id":"724e8238-2ed5-4089-9509-9c7c12a3c373"}
[2026-05-07 12:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"172d1ae8-b8cc-4804-bed9-e32d074e265c","trace_id":"4817cdef-8d3e-4914-8bee-feffb18efe1b"}
[2026-05-07 12:28:14] 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":"172d1ae8-b8cc-4804-bed9-e32d074e265c","trace_id":"4817cdef-8d3e-4914-8bee-feffb18efe1b"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1499,"provider":"hubspot","refreshToken":"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86","state":"connected"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.NOTICE: Monitoring start {"correlation_id":"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5","trace_id":"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72"}
[2026-05-07 12:28:18] local.NOTICE: Monitoring end {"correlation_id":"31aa6b7d-00d6-448a-bdcc-a3d215b4aeb5","trace_id":"b8eb7fe5-8471-4ffb-9bfa-a804c8e1ff72"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [SocialAccountService] Token refreshed {"socialAccountId":1499,"provider":"hubspot","state":"connected"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:18] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:19] local.INFO: [Hubspot] Failed to fetch opportunity {"crm_id":"374720564","reason":"[429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)
"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:19] local.ERROR: [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{"status":"error","message":"You have reached your ten_secondly_rolling limit.","errorType":"RATE_LIMIT","correlationId" (truncated...)
{"exception":"[object] (HubSpot\\Client\\Crm\\Deals\\ApiException(code: 429): [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)
at /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php(676): HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi->getByIdWithHttpInfo('374720564', 'hs_object_id,de...', 'companies,conta...', false, NULL)
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(212): HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi->getById('374720564', 'hs_object_id,de...', 'companies,conta...')
#2 /home/jiminny/app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php(130): Jiminny\\Services\\Crm\\Hubspot\\Client->getOpportunityById('374720564', Array)
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(351): Jiminny\\Services\\Crm\\Hubspot\\Service->syncOpportunity('374720564')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
"} {"correlation_id":"273355f2-6315-4b20-bc9a-c26c681d6344","trace_id":"5ea79a26-c838-48e8-9913-93ad508146a6"}
[2026-05-07 12:28:20] 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":"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d","trace_id":"1ea62b83-9639-41e3-a523-26404c39fa80"}
[2026-05-07 12:28:20] 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":"dcb9b24b-e2e5-41fe-81db-212b3ca9ff7d","trace_id":"1ea62b83-9639-41e3-a523-26404c39fa80"}
[2026-05-07 12:28:24] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:24] 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":"d191d5c6-133f-43e1-8ed4-d285d0c768b8","trace_id":"67798138-bdb7-4bd7-8b32-377663219a88"}
[2026-05-07 12:28:27] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 12:26:00, 2026-05-07 12:28:00] {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:27] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"a4e5e18f-9f8c-4196-ace0-bf664d4827d9","trace_id":"d75cb10b-4c76-4c00-84d4-26efeb738e17"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"calendar:sync","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"3789f06a-4f0c-4b12-be80-d6a36e089d1b","trace_id":"6ce89147-1624-456d-9e75-f4948f2c5db8"}
[2026-05-07 12:28:30] local.NOTICE: Calendar sync start {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:30] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"3789f06a-4f0c-4b12-be80-d6a36e089d1b","trace_id":"6ce89147-1624-456d-9e75-f4948f2c5db8"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1393,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1393,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1393,"provider":"google","refreshToken":"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1387,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1387,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1387,"provider":"google","refreshToken":"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1348,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1348,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1348,"provider":"google","refreshToken":"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1361,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1361,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1361,"provider":"google","refreshToken":"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1310,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1310,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1310,"provider":"google","refreshToken":"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1333,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1333,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1333,"provider":"google","refreshToken":"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1368,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1368,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1368,"provider":"google","refreshToken":"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1365,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1365,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:32] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1365,"provider":"google","refreshToken":"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1364,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1364,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1364,"provider":"google","refreshToken":"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1370,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1370,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1370,"provider":"office","refreshToken":"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] 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: e890fdc1-dbe8-4a59-ae57-2af6bced3c00 Correlation ID: 57554cdf-16df-47f1-b0d9-5f0b8da37afe Timestamp: 2026-05-07 12:28:33Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:33Z\",\"trace_id\":\"e890fdc1-dbe8-4a59-ae57-2af6bced3c00\",\"correlation_id\":\"57554cdf-16df-47f1-b0d9-5f0b8da37afe\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1202,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1202,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:33] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1202,"provider":"office","refreshToken":"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] 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: 2a3e5b60-5770-46f2-aca6-7b0527363000 Correlation ID: 57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2 Timestamp: 2026-05-07 12:28:34Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:34Z\",\"trace_id\":\"2a3e5b60-5770-46f2-aca6-7b0527363000\",\"correlation_id\":\"57bb0b73-9ea6-4bcf-a8f4-6e211dbb94e2\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: Calendar sync job dispatched {"calendar_id":501} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1300,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1300,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1300,"provider":"google","refreshToken":"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:34] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1409,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1409,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1409,"provider":"google","refreshToken":"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1352,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1352,"provider":"google"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1352,"provider":"google","refreshToken":"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1296,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1296,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1296,"provider":"office","refreshToken":"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] 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":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"integration-app","crm_owner":1695,"team_id":3143} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] 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: 2e34e335-a76d-40e5-bc50-3861392e4c00 Correlation ID: 9db5b389-6165-4feb-8e6e-bedc369e1c87 Timestamp: 2026-05-07 12:28:35Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:35Z\",\"trace_id\":\"2e34e335-a76d-40e5-bc50-3861392e4c00\",\"correlation_id\":\"9db5b389-6165-4feb-8e6e-bedc369e1c87\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":391,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":391,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:35] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":391,"provider":"office","refreshToken":"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.INFO: [Google Calendar] Failed to watch channel for calendar {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.WARNING: [Calendar] Sync failed {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"3015918c-9edf-487d-b1d0-97c9d00ea6b1","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] 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: 6eba7173-b781-4e55-b1fb-1087ed023000 Correlation ID: 3086e346-c6eb-4f1c-8b1d-a477ce3821f4 Timestamp: 2026-05-07 12:28:36Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-07 12:28:36Z\",\"trace_id\":\"6eba7173-b781-4e55-b1fb-1087ed023000\",\"correlation_id\":\"3086e346-c6eb-4f1c-8b1d-a477ce3821f4\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:36] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1271,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1271,"provider":"office"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1271,"provider":"office","refreshToken":"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb","state":"full-refresh"} {"correlation_id":"d543a089-fd32-4522-9ffe-1494e562b741","trace_id":"564a362b-7ddb-49f0-a33a-b807b0440231"}
[2026-05-07 12:28:37] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1271,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
3639
|
133
|
8
|
2026-05-07T12:32:19.547433+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778157139547_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/…/SyncCrmEntitiesTrait.php
|
1
|
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
Search History
Hubspot
New Line...
|
[{"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<br/>Some incoming commits are not fetched<br/>","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":"AXCheckBox","text":"Search History","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Hubspot","depth":4,"on_screen":true,"value":"Hubspot","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3003060951785363827
|
-8204349974642447424
|
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
Search History
Hubspot
New Line
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(wbl• Lukas/Stefka 121 - in 1h 58 m100%8DEV (docker)Thu 7 May 15:32:19181₴6DOCKERDEV (docker)H82worker-download:worker-download_00:startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedroot@docker_lamp_1:/home/jiminny#php artisan jiminny: debugSyncing opportunity 0Syncing opportunity 25Syncing opportunity 50Syncing opportunity 75Syncingopportunity 100root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugSyncing opportunity 0Syncing opportunity 25Syncing opportunity 50Syncing opportunity 75Syncing opportunity 100root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugSyncing opportunity 0APP (-zsh)-zsh• 84|screenpipe*-zshDEVHubSpot\Client\Crm\Deals\ApiException[429] Client error: *GET [URL_WITH_CREDENTIALS] ]...
|
3637
|
NULL
|
NULL
|
NULL
|
|
88403
|
3015
|
5
|
2026-05-28T17:01:49.705452+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987709705_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-l© rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-l© Service Test.phpHubspotClientinterface.phC) Team.phd© HubspotTokenManager.pt© PayloadBuilder.phpoKimol Cimosewhoowusoswosnoocea.cokicontoeuisynostoroenC) PayloadBulider.oho© CiosedDealStagesService.ohg© CrmEntitvReoository.ohdResponseNormalize.phoCSeMCPono© SyncFieldAction.ohoCSWnCKealCohe wWwKexisiinastaad10.02.23 Vasilev24.01.25 Papazov© WebhookSvncBatchProce3862.04.10Grandnclass Service extends BaseService implements01 A7 A149 V1V33 /1 A v 170%public function inportstages (Parray_Stypes u.novw, Psteing SmissingStageNane v,nuLl): 25tage= busznessrrocess..lYre UrPUKtUNelY1706— 17081716= Sof'actsive'1sraveloe# HS local [liminny@localhostconsole (STAGINGA console (EU) x iii users (EU)listeners> MetadataaMicrationP oedriveEh SalesforcealeeldsaOpoortunityVatchenOpportunitySyncStrategyProspectSearchStrateg.sametiteGransth19.08.18 GrahamMonowa GrhM70418— 1715select * fron activities where id = 31264367= Sthis->tean->id,enh ctoimmdthsar nhetdWothS8)-1728select * fron contacts where id = 6331639:seleekx tron account anchs 0-450054= 1726select * fron opportunities where id = 4843610:i salectable= Sol'active'l#Uodare300n8-4300-4"contact_id' ="busiiness pnocessid' = ShusinessProcesse>id2% nuaulalmaeoratdett ooe8.11.1( DeletcObiectsTrait.php2.04.18Graham#"stage_id' = 13273"updated.at" = 2826-95-22 07:16:=12219.03.18 Grahamoewanarinithooe nha-1724select * fron text relavs where created at > '2926-95-91*:Stages - fetch all existing stages upfront to avoid N+1 queries© PayloadBuilder.phpes = Sthis-›config-›stages®c) Profile.phpselect * fron actvales order oy 1d desca4.05.26428-sithirashed®© QueryBuilder.phpAnsas-sahere("type', Stage::TYPE OPPORTUNITY)I1727IIl© QueryHandler.php4.05.26eQuerviterator.oh430select * fron users where nane uike subras:-sget->keyBy(*crm provider 1d')=1729© QueryResults.php4.05.26© Service.php432=1730SELECT * FROM opportunities HERE unid to_ bin('84a9cfad-2c87-4453-S|2.10.25foreach (Sp"""stages' as SdealStage)17391© SyncBatchRedisService.pt3.10.25434Ss = ResponseNornalize::normalizeDealStage(SdealStage):E1732select * fron teans where 1d= 555%select * fron stages where tean_1d = 555:h Traits2.10.25—1733SETSTMTRaseeentono4.05.26© BaseService.cho436/x* Bvac ?Stage SexistingStage */=1735CONCAT(U.Ld, CASE WHEN U.1d = t.ouner_id THEN" (owner)" ELSEMesanSexistingStage = SexistingStages->get(Ss('id'))© CachedCrmServiceDecoratoAosos4.05.2043₴#122onnenstil socal nccountee4.05.20I Restore soft-deleted stages that are now active in HubSpotCrmActivityProviderinteorate1738JOIN users u on u.id = sa.sociable_id4.05.20446if (SexistingStage?->trashed") && Ss('active')) 4CCnlACMiWMoh= 1755CermcontcurationSettnosserSexfatinaStage-restoreo,ahittinnetneosonetts1703WHERE u.tean_id = 100 and sa.provider = 'hubspot":©rmobiectsteso wer.ono405.26=174C. DefaultProsoectSearchStrate=1742I/ Josent stage (uodates soft-deleted records without restonina then)C mallteloer.ond3sindeProscectinternce.ono445)Sstage = Sthis->config->stages()->withTrashed()->update0rCreateCl= 1143select id, is_closed, is_won, stage_updated.at, crm_provider_id, stclose date, forecast_category, deleted_at, created_at, remotely_crtfron opportunities where tean id = SSS and stage_id = 28616 orderonorowider 1 8 SsldoC) LavouMansoe onoMatchdomsinsysmainten1. T40528C Opportun tvActvitwlatcheeweh S8= 17431746 %—1/40select id. crn-provider id, nane, probability, undated at. deletesfron stageswhere id = 28616eennortur weundCtstomodeHe OrnenontCostrhSrond nhnvhrecoroewoet relklyclone orne nusness lorocessy useerostonsnichrecoreus// Create/update record type clondSthis->confta->recondTvpes()->update0rCoeatecf=> $p['id']Sthrisosteanosdl= nb_strimwidth($s["label'].nh strimmathScht'aher".=> Stage:: TYPE_OPPORTUNITY=> Ss['display0rder'],"is selectahle" as Sef'active'l.Tx: AutovBROER PYTnane, M.emareSo liminnyv031 A9 A29 V3 /109 A VSELECT * FRON tEaNS WHERE name LIKE "Stounlanes: # 187, 289, 8158SEEiNTCONCAT(U.1d, CASE WHEN U.10 = t.ouner_1d THEN" (owner)" ELSEMrenasisa.*t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_1dWHERE u.tean_id = 187 and sa.provider = 'salesfonce':TO0У L7Thu 28 May 20:01:49ServiceTestCascadeles Orcnnworeebee.ionHlmwesotino ooro+0.• git show 5fd72@fd5b:app/Services/Crm/Hubspot/Service.php | grep -A 78 "function importStages" | tail -2:1. nCm_provider_id* o Sst*10')"tean_1dS gee pneer JHEY,The probability logic was identical in Nov 2025. Now let me chock normalizebealStage at that time:• qit shơw 51d7201d5b:app/Services/Crm/Hubspot/ResponseNormalize.php | grep -A 20 *normalizeDealStage)puotie statae tunetion noreatazevedistagetarray fobgect soeatstage: arrayhtArCe eorAMNenYaoereturn!b soatal-tabelAiso idenacal. now let me check tne getdea ispipe linestnopoint - this is wnät tne nov zuzo ou x import useo lo leich o peline oata, was there aditterent endoc nt be no vend at that timet•git log -oneline →alla/Services/Cre/Hubspot/Service-php/ xargs - sh -Cglt shou /dewtontl 1 boad -eair loo -onelin-1t1-4AlteraM - aon/Services/Cmm/Hubspot/Service.oho | head -2Bestore 1oeei tod snpge, restore in recrestedAdd permissionTmportBusinessProcesses. ...... toe oll coue tut colactarcoLesentation• qit log -oneline -before="2025-11-19" -- app/Services/Crm/Hubspot/Service.php | head -350740e988 39-9625 Tix Sr verification - check Task cor Event oy olaybodAsk anything (XOL)• OKwodturlasmeXekwiirest4 spag...
|
NULL
|
-448637763111002668
|
NULL
|
click
|
ocr
|
NULL
|
rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-l© rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-l© Service Test.phpHubspotClientinterface.phC) Team.phd© HubspotTokenManager.pt© PayloadBuilder.phpoKimol Cimosewhoowusoswosnoocea.cokicontoeuisynostoroenC) PayloadBulider.oho© CiosedDealStagesService.ohg© CrmEntitvReoository.ohdResponseNormalize.phoCSeMCPono© SyncFieldAction.ohoCSWnCKealCohe wWwKexisiinastaad10.02.23 Vasilev24.01.25 Papazov© WebhookSvncBatchProce3862.04.10Grandnclass Service extends BaseService implements01 A7 A149 V1V33 /1 A v 170%public function inportstages (Parray_Stypes u.novw, Psteing SmissingStageNane v,nuLl): 25tage= busznessrrocess..lYre UrPUKtUNelY1706— 17081716= Sof'actsive'1sraveloe# HS local [liminny@localhostconsole (STAGINGA console (EU) x iii users (EU)listeners> MetadataaMicrationP oedriveEh SalesforcealeeldsaOpoortunityVatchenOpportunitySyncStrategyProspectSearchStrateg.sametiteGransth19.08.18 GrahamMonowa GrhM70418— 1715select * fron activities where id = 31264367= Sthis->tean->id,enh ctoimmdthsar nhetdWothS8)-1728select * fron contacts where id = 6331639:seleekx tron account anchs 0-450054= 1726select * fron opportunities where id = 4843610:i salectable= Sol'active'l#Uodare300n8-4300-4"contact_id' ="busiiness pnocessid' = ShusinessProcesse>id2% nuaulalmaeoratdett ooe8.11.1( DeletcObiectsTrait.php2.04.18Graham#"stage_id' = 13273"updated.at" = 2826-95-22 07:16:=12219.03.18 Grahamoewanarinithooe nha-1724select * fron text relavs where created at > '2926-95-91*:Stages - fetch all existing stages upfront to avoid N+1 queries© PayloadBuilder.phpes = Sthis-›config-›stages®c) Profile.phpselect * fron actvales order oy 1d desca4.05.26428-sithirashed®© QueryBuilder.phpAnsas-sahere("type', Stage::TYPE OPPORTUNITY)I1727IIl© QueryHandler.php4.05.26eQuerviterator.oh430select * fron users where nane uike subras:-sget->keyBy(*crm provider 1d')=1729© QueryResults.php4.05.26© Service.php432=1730SELECT * FROM opportunities HERE unid to_ bin('84a9cfad-2c87-4453-S|2.10.25foreach (Sp"""stages' as SdealStage)17391© SyncBatchRedisService.pt3.10.25434Ss = ResponseNornalize::normalizeDealStage(SdealStage):E1732select * fron teans where 1d= 555%select * fron stages where tean_1d = 555:h Traits2.10.25—1733SETSTMTRaseeentono4.05.26© BaseService.cho436/x* Bvac ?Stage SexistingStage */=1735CONCAT(U.Ld, CASE WHEN U.1d = t.ouner_id THEN" (owner)" ELSEMesanSexistingStage = SexistingStages->get(Ss('id'))© CachedCrmServiceDecoratoAosos4.05.2043₴#122onnenstil socal nccountee4.05.20I Restore soft-deleted stages that are now active in HubSpotCrmActivityProviderinteorate1738JOIN users u on u.id = sa.sociable_id4.05.20446if (SexistingStage?->trashed") && Ss('active')) 4CCnlACMiWMoh= 1755CermcontcurationSettnosserSexfatinaStage-restoreo,ahittinnetneosonetts1703WHERE u.tean_id = 100 and sa.provider = 'hubspot":©rmobiectsteso wer.ono405.26=174C. DefaultProsoectSearchStrate=1742I/ Josent stage (uodates soft-deleted records without restonina then)C mallteloer.ond3sindeProscectinternce.ono445)Sstage = Sthis->config->stages()->withTrashed()->update0rCreateCl= 1143select id, is_closed, is_won, stage_updated.at, crm_provider_id, stclose date, forecast_category, deleted_at, created_at, remotely_crtfron opportunities where tean id = SSS and stage_id = 28616 orderonorowider 1 8 SsldoC) LavouMansoe onoMatchdomsinsysmainten1. T40528C Opportun tvActvitwlatcheeweh S8= 17431746 %—1/40select id. crn-provider id, nane, probability, undated at. deletesfron stageswhere id = 28616eennortur weundCtstomodeHe OrnenontCostrhSrond nhnvhrecoroewoet relklyclone orne nusness lorocessy useerostonsnichrecoreus// Create/update record type clondSthis->confta->recondTvpes()->update0rCoeatecf=> $p['id']Sthrisosteanosdl= nb_strimwidth($s["label'].nh strimmathScht'aher".=> Stage:: TYPE_OPPORTUNITY=> Ss['display0rder'],"is selectahle" as Sef'active'l.Tx: AutovBROER PYTnane, M.emareSo liminnyv031 A9 A29 V3 /109 A VSELECT * FRON tEaNS WHERE name LIKE "Stounlanes: # 187, 289, 8158SEEiNTCONCAT(U.1d, CASE WHEN U.10 = t.ouner_1d THEN" (owner)" ELSEMrenasisa.*t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_1dWHERE u.tean_id = 187 and sa.provider = 'salesfonce':TO0У L7Thu 28 May 20:01:49ServiceTestCascadeles Orcnnworeebee.ionHlmwesotino ooro+0.• git show 5fd72@fd5b:app/Services/Crm/Hubspot/Service.php | grep -A 78 "function importStages" | tail -2:1. nCm_provider_id* o Sst*10')"tean_1dS gee pneer JHEY,The probability logic was identical in Nov 2025. Now let me chock normalizebealStage at that time:• qit shơw 51d7201d5b:app/Services/Crm/Hubspot/ResponseNormalize.php | grep -A 20 *normalizeDealStage)puotie statae tunetion noreatazevedistagetarray fobgect soeatstage: arrayhtArCe eorAMNenYaoereturn!b soatal-tabelAiso idenacal. now let me check tne getdea ispipe linestnopoint - this is wnät tne nov zuzo ou x import useo lo leich o peline oata, was there aditterent endoc nt be no vend at that timet•git log -oneline →alla/Services/Cre/Hubspot/Service-php/ xargs - sh -Cglt shou /dewtontl 1 boad -eair loo -onelin-1t1-4AlteraM - aon/Services/Cmm/Hubspot/Service.oho | head -2Bestore 1oeei tod snpge, restore in recrestedAdd permissionTmportBusinessProcesses. ...... toe oll coue tut colactarcoLesentation• qit log -oneline -before="2025-11-19" -- app/Services/Crm/Hubspot/Service.php | head -350740e988 39-9625 Tix Sr verification - check Task cor Event oy olaybodAsk anything (XOL)• OKwodturlasmeXekwiirest4 spag...
|
88401
|
NULL
|
NULL
|
NULL
|
|
88402
|
3014
|
2
|
2026-05-28T17:01:49.809944+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987709809_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
HomeDMsActivityFilesLater..•More+Slack> 0(ah]Fi HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:01:49®8 10Untitled +...
|
NULL
|
71071965043401855
|
NULL
|
click
|
ocr
|
NULL
|
HomeDMsActivityFilesLater..•More+Slack> 0(ah]Fi HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:01:49®8 10Untitled +...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
88401
|
3015
|
4
|
2026-05-28T17:01:48.586223+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987708586_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
1
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'ac...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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":"Show Replace Field","depth":4,"bounds":{"left":0.11868351,"top":0.15482841,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.13131648,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"existingStages","depth":4,"bounds":{"left":0.14228724,"top":0.15403032,"width":0.05319149,"height":0.015961692},"on_screen":true,"value":"existingStages","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.2044548,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.21442819,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.22307181,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.23171543,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/2","depth":4,"bounds":{"left":0.24534574,"top":0.15323225,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.27094415,"top":0.15243416,"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":"Next Occurrence","depth":4,"bounds":{"left":0.27958778,"top":0.15243416,"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":"Filter Search Results","depth":4,"bounds":{"left":0.28823137,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.296875,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.46210107,"top":0.15243416,"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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.39727393,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.40658244,"top":0.18355946,"width":0.0076462766,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"149","depth":4,"bounds":{"left":0.4162234,"top":0.18355946,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.43018618,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.43949467,"top":0.18355946,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.45179522,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.46077126,"top":0.1819633,"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.4680851,"top":0.1819633,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse HubSpot\\Client\\Crm\\Owners\\Model\\PublicOwner;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->handleOwnersApiException($e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n $profile = $this->processOwner($owner, $teamRepository, $profileRepository);\n\n if ($profile && $userToSearch && $userToSearch->getId() === $profile->getUserId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function handleOwnersApiException(\\HubSpot\\Client\\Crm\\Owners\\ApiException $e): never\n {\n $statusCode = $e->getCode();\n $errorMessage = $e->getMessage();\n $responseBody = $this->parseResponseBody($e->getResponseBody());\n\n $isPermissionError = $this->isPermissionError($statusCode, $errorMessage, $responseBody);\n\n $logContext = [\n 'team_id' => $this->team->getId(),\n 'team_uuid' => $this->team->getUuid(),\n 'config_id' => $this->config->getId(),\n 'status_code' => $statusCode,\n 'error_message' => $errorMessage,\n 'response_body' => $responseBody,\n ];\n\n if ($isPermissionError) {\n $this->logPermissionError($logContext);\n } else {\n $this->logger->error('[HubSpot] Could not sync the profiles.', $logContext);\n }\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n private function parseResponseBody(?string $rawBody): ?array\n {\n if ($rawBody === null || $rawBody === '') {\n return null;\n }\n\n try {\n return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n } catch (\\JsonException) {\n return null;\n }\n }\n\n private function logPermissionError(array $logContext): void\n {\n $this->logger->critical(\n '[HubSpot] ⚠️ PERMISSION ERROR: Cannot sync profiles - Missing OAuth scopes',\n array_merge($logContext, [\n 'action_required' => 'Request additional HubSpot OAuth scopes',\n 'required_scope' => 'crm.objects.owners.read',\n 'impact' => 'Opportunities will have owner_id but NO user_id - AI automation and reporting will fail',\n 'resolution' => 'Manually request scope from HubSpot account admin or re-authenticate',\n ])\n );\n }\n\n private function processOwner(\n PublicOwner $owner,\n TeamRepository $teamRepository,\n ProfileRepository $profileRepository\n ): ?Profile {\n if ($owner->getArchived()) {\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n return null;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n return null;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n if (! $user instanceof User) {\n return null;\n }\n\n return $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n }\n\n private function isPermissionError(int $statusCode, string $errorMessage, ?array $responseBody): bool\n {\n // Check HTTP 403 Forbidden\n if ($statusCode === 403) {\n return true;\n }\n\n // Check for scope-related keywords in error message\n $scopeKeywords = ['scope', 'permission', 'forbidden', 'unauthorized', 'access denied', 'oauth'];\n $lowerErrorMessage = strtolower($errorMessage);\n\n foreach ($scopeKeywords as $keyword) {\n if (str_contains($lowerErrorMessage, $keyword)) {\n return true;\n }\n }\n\n // Check response body for scope errors\n if ($responseBody !== null) {\n return $this->arrayContainsKeyword($responseBody, $scopeKeywords);\n }\n\n return false;\n }\n\n /**\n * Recursively search array for keywords in values\n */\n private function arrayContainsKeyword(array $data, array $keywords): bool\n {\n foreach ($data as $value) {\n if (is_array($value)) {\n if ($this->arrayContainsKeyword($value, $keywords)) {\n return true;\n }\n } elseif (is_string($value)) {\n $lowerValue = strtolower($value);\n foreach ($keywords as $keyword) {\n if (str_contains($lowerValue, $keyword)) {\n return true;\n }\n }\n }\n }\n\n return false;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse HubSpot\\Client\\Crm\\Owners\\Model\\PublicOwner;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->handleOwnersApiException($e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n $profile = $this->processOwner($owner, $teamRepository, $profileRepository);\n\n if ($profile && $userToSearch && $userToSearch->getId() === $profile->getUserId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function handleOwnersApiException(\\HubSpot\\Client\\Crm\\Owners\\ApiException $e): never\n {\n $statusCode = $e->getCode();\n $errorMessage = $e->getMessage();\n $responseBody = $this->parseResponseBody($e->getResponseBody());\n\n $isPermissionError = $this->isPermissionError($statusCode, $errorMessage, $responseBody);\n\n $logContext = [\n 'team_id' => $this->team->getId(),\n 'team_uuid' => $this->team->getUuid(),\n 'config_id' => $this->config->getId(),\n 'status_code' => $statusCode,\n 'error_message' => $errorMessage,\n 'response_body' => $responseBody,\n ];\n\n if ($isPermissionError) {\n $this->logPermissionError($logContext);\n } else {\n $this->logger->error('[HubSpot] Could not sync the profiles.', $logContext);\n }\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n private function parseResponseBody(?string $rawBody): ?array\n {\n if ($rawBody === null || $rawBody === '') {\n return null;\n }\n\n try {\n return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n } catch (\\JsonException) {\n return null;\n }\n }\n\n private function logPermissionError(array $logContext): void\n {\n $this->logger->critical(\n '[HubSpot] ⚠️ PERMISSION ERROR: Cannot sync profiles - Missing OAuth scopes',\n array_merge($logContext, [\n 'action_required' => 'Request additional HubSpot OAuth scopes',\n 'required_scope' => 'crm.objects.owners.read',\n 'impact' => 'Opportunities will have owner_id but NO user_id - AI automation and reporting will fail',\n 'resolution' => 'Manually request scope from HubSpot account admin or re-authenticate',\n ])\n );\n }\n\n private function processOwner(\n PublicOwner $owner,\n TeamRepository $teamRepository,\n ProfileRepository $profileRepository\n ): ?Profile {\n if ($owner->getArchived()) {\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n return null;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n return null;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n if (! $user instanceof User) {\n return null;\n }\n\n return $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n }\n\n private function isPermissionError(int $statusCode, string $errorMessage, ?array $responseBody): bool\n {\n // Check HTTP 403 Forbidden\n if ($statusCode === 403) {\n return true;\n }\n\n // Check for scope-related keywords in error message\n $scopeKeywords = ['scope', 'permission', 'forbidden', 'unauthorized', 'access denied', 'oauth'];\n $lowerErrorMessage = strtolower($errorMessage);\n\n foreach ($scopeKeywords as $keyword) {\n if (str_contains($lowerErrorMessage, $keyword)) {\n return true;\n }\n }\n\n // Check response body for scope errors\n if ($responseBody !== null) {\n return $this->arrayContainsKeyword($responseBody, $scopeKeywords);\n }\n\n return false;\n }\n\n /**\n * Recursively search array for keywords in values\n */\n private function arrayContainsKeyword(array $data, array $keywords): bool\n {\n foreach ($data as $value) {\n if (is_array($value)) {\n if ($this->arrayContainsKeyword($value, $keywords)) {\n return true;\n }\n } elseif (is_string($value)) {\n $lowerValue = strtolower($value);\n foreach ($keywords as $keyword) {\n if (str_contains($lowerValue, $keyword)) {\n return true;\n }\n }\n }\n }\n\n return false;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.47672874,"top":0.123703115,"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.48537233,"top":0.123703115,"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.49634308,"top":0.123703115,"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.5049867,"top":0.123703115,"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.51363033,"top":0.123703115,"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.52460104,"top":0.123703115,"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.5355718,"top":0.123703115,"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.5621675,"top":0.123703115,"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.5731383,"top":0.123703115,"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.64261967,"top":0.123703115,"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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"31","depth":4,"bounds":{"left":0.60039896,"top":0.14844373,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.61203456,"top":0.14844373,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"29","depth":4,"bounds":{"left":0.62200797,"top":0.14844373,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.6343085,"top":0.14844373,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"109","depth":4,"bounds":{"left":0.6442819,"top":0.14844373,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.65791225,"top":0.14684756,"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.66522604,"top":0.14684756,"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":"SELECT * FROM team_features where team_id = 1;\n\nSELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922\nSELECT * FROM users WHERE team_id = 340; # 12015\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 340\nand sa.provider = 'salesforce';\n# and sa.provider = 'salesloft';\n\nselect * from crm_fields where crm_configuration_id = 270 and object_type = 'event';\n# 125558 - Event Type - Event_Type__c\n# 125552 - Event Status - Event_Status__c\n\nSELECT * FROM sidekick_settings WHERE team_id = 340;\n\nSELECT * FROM crm_field_values WHERE crm_field_id in (125552);\n\nselect * from activities where crm_configuration_id = 270\nand type = 'conference' and crm_provider_id IS NOT NULL\nand actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;\n\nSELECT * FROM activities WHERE id = 20871677;\nSELECT * FROM crm_field_data WHERE activity_id = 20871677;\n\nselect * from crm_layouts where crm_configuration_id = 270;\nselect * from crm_layout_entities where crm_layout_id in (886,887);\n\nSELECT * FROM crm_configurations WHERE id = 270;\n\nselect * from playbooks where team_id = 340; # 1514\nselect * from groups where team_id = 340;\nSELECT * FROM crm_fields WHERE id IN (125393, 125401);\n\nselect g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g\njoin playbooks p on g.playbook_id = p.id\njoin crm_fields f on p.activity_field_id = f.id\nwhere g.team_id = 340;\n\nSELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716\nselect * from crm_field_data where object_id = 20448716;\n\nselect * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008\nselect * from opportunities where team_id = 343;\nselect * from opportunities where team_id = 343 and crm_provider_id = '18099102526';\nselect * from opportunities where team_id = 343 and account_id = 945217482;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from accounts where team_id = 343 order by name asc;\n\nselect * from stages where crm_configuration_id = 273 and type = 'opportunity';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143\nSELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;\nSELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';\nSELECT * FROM activities WHERE id = 20717903;\n\nselect * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 353\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, l.atkinson@mwbsolutions.co.uk\nSELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;\n# id: 20940638, user: 12022, contact: 5305871\nSELECT * FROM activity_summary_logs WHERE activity_id = 20940638;\nselect * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 345\nand sa.provider = 'hubspot';\n\nselect * from users where team_id = 345 and id = 12022;\nSELECT * FROM crm_profiles WHERE user_id = 12022;\nSELECT * FROM participants WHERE activity_id = 20940638;\nSELECT * FROM users u\nJOIN crm_profiles cp ON u.id = cp.user_id\nWHERE u.team_id = 345;\n\nselect * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871\n\nselect * from team_features where team_id = 345;\nSELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197\nSELECT * FROM participants WHERE activity_id = 20897406;\n\n\n\nSELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912\nSELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';\n\n\nSELECT * FROM activities WHERE id = 20946641;\nSELECT * FROM crm_profiles WHERE user_id = 10211;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, triger@lunio.ai\nSELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';\nselect * from stages where crm_configuration_id = 97 and type = 'opportunity';\nselect * from opportunities where team_id = 120;\n\n\nselect * from crm_configurations crm join teams t on crm.id = t.crm_id\nwhere 1=1\nAND t.current_billing_plan IS NOT NULL\nAND crm.auto_sync_activity = 0\nand crm.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,james.lewendon@exclaimer.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 270\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956\nSELECT * FROM crm_profiles WHERE user_id = 11446;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, alex.chikly@cygnetise.com\nselect * from playbooks where team_id = 372;\nselect * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340\nSELECT * FROM crm_field_values WHERE crm_field_id = 141340;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 372\nand sa.provider = 'salesforce';\n\nselect * from crm_profiles where crm_configuration_id = 300;\nSELECT * FROM crm_configurations WHERE team_id = 372;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,mfa@planday.com\nSELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756\nselect * from crm_field_data where object_id = 3207756;\nSELECT * FROM crm_fields WHERE id = 111834;\n\nselect f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value\nFROM crm_fields f\nJOIN crm_field_data fd ON f.id = fd.crm_field_id\nWHERE f.crm_configuration_id = 242\nAND f.object_type = 'opportunity'\nAND fd.object_id IN (3207756)\nORDER BY fd.object_id, fd.updated_at;\n\nSELECT * FROM crm_configurations WHERE auto_connect = 1;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,salesforce-admin@tourlane.com\nselect * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id\nwhere g.team_id = 187;\n\nselect * from `groups` where team_id = 187;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 187\nand sa.provider = 'salesforce';\n\n# Destination - 98870 - Destination__c\n# Stage - 79014 - StageName\n# Land Arrangement - 98856 - Land_Arrangement__c\n# Flight - 98848 - Flight__c\n# Last activity date - 98812 - LastActivityDate\n# Last modified date - 98809 - LastModifiedDate\n# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c\n# next call - 98864 - Next_Call__c\n\nselect * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\nselect * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';\nselect * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;\nselect * from activities where opportunity_id = 3538248;\n\nSELECT * FROM crm_profiles WHERE user_id = 8150;\n\nselect * from deal_risks where opportunity_id = 3538248;\n\nselect * from teams where crm_id IS NULL;\n\nSELECT opp.id AS opportunity_id,\n u.group_id AS group_id,\n MAX(\n CASE\n WHEN a.type IN (\"sms-inbound\", \"sms-outbound\") THEN a.created_at\n ELSE a.actual_end_time\n END) as last_date\nFROM opportunities opp\nleft join activities a on a.opportunity_id = opp.id\ninner join users u on opp.user_id = u.id\nwhere opp.user_id IN (9951)\n\nAND opp.is_closed = 0\nand a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL\ngroup by opp.id;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,polly.morphew@cybsafe.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 301;\nSELECT * FROM contacts WHERE id = 6612363;\nSELECT * FROM accounts WHERE id = 4235676;\nSELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;\nselect * from opportunity_stages where opportunity_id = 4503759;\n# SELECT * FROM opportunities WHERE id = 4569937;\n\nselect * from activities where crm_configuration_id = 301;\nSELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370\nSELECT * FROM participants WHERE activity_id = 26330370;\n\nSELECT * FROM teams WHERE id = 375;\nselect * from playbooks where team_id = 375;\n\nselect * from stages where crm_configuration_id = 301 and type = 'opportunity';\n\nselect * from teams;\nselect * from contact_roles;\n\nSELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';\n\nselect * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;\n\nSELECT * FROM crm_field_data WHERE object_id = 3771706;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'\nand crm_provider_id LIKE \"%traffic_light%\";\nSELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);\n\nSELECT fd.* FROM opportunities o\nJOIN crm_field_data fd ON o.id = fd.object_id\nWHERE o.team_id = 343\n# and o.user_id IS NOT NULL\nand fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)\nand fd.value != ''\norder by value desc\n# group by o.id\n;\n\nSELECT * FROM opportunities WHERE id = 3769843;\n\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, salesforce-admin@tourlane.com\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,aswini.mishra@fundingcircle.com\nSELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839\n\n\nSELECT * FROM opportunities WHERE id = 3855992;\n\nSELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988\n\nSELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';\n\nselect * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507\nSELECT * FROM crm_field_data WHERE object_id = 5874411;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379\nand sa.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, nikhil.kumar@mention-me.com\nSELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\nSELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793\nselect * from generic_ai_prompts where subject_id = 3537793;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, triger@lunio.ai\nSELECT * FROM crm_configurations WHERE id = 97;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 97;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;\nSELECT * FROM crm_fields WHERE id = 32682;\n\nselect cfd.value, o.* from opportunities o\njoin crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682\nwhere team_id = 120\nand cfd.value != ''\n;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 120\nand sa.provider = 'salesforce';\n\nselect * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';\nSELECT * FROM crm_field_data WHERE object_id = 2313439;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 410;\nSELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';\nselect * from scorecards where team_id = 410;\nselect * from scorecard_rules;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, aswini.mishra@fundingcircle.com\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\njoin users u on o.user_id = u.id\nwhere a.crm_configuration_id = 177 and a.type LIKE '%email-out%'\n# and a.actual_end_time > '2024-12-16 00:00:00'\n# and o.remotely_created_at > '2024-12-01 00:00:00'\n# and u.group_id = 1014\nand u.id = 9021\norder by a.id desc;\nSELECT * FROM opportunities WHERE id in (3981384,4017346);\nSELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);\n\nselect * from users where id = 9021;\nselect * from inboxes where user_id = 9021;\n\nselect * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';\n\nselect * from email_messages where team_id = 220\nand orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'\nand subject LIKE '%Personal%'\n# and 'from' = 'credit@fundingcircle.com'\n;\n\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\nwhere a.user_id = 9021 and a.type LIKE '%email-out%'\nand a.actual_end_time > '2024-12-18 00:00:00'\nand o.user_id IS NOT NULL\nand o.remotely_created_at > '2024-12-01 00:00:00'\norder by a.id desc;\n\nSELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;\nselect * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;\n\nselect * from team_settings where name IN ('useCloseDate');\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, jfarrell@hurree.co\nSELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 104\nand sa.provider = 'hubspot';\n\nselect * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'\nselect * from teams where crm_id IS NULL;\n\nselect t.name as 'team', u.name as 'owner', u.email, u.phone\nfrom teams t\njoin activity_providers ap on t.id = ap.team_id\njoin users u on t.owner_id = u.id\nwhere 1=1\n and t.status = 'active'\n and ap.is_enabled = 1\n# and u.status = 1\n and ap.provider = 'ms-teams';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nSELECT * FROM teams WHERE id = 442; # 14293\nselect * from users where team_id = 442;\nselect * from social_accounts sa where sa.sociable_id = 14293;\nselect * from invitations where team_id = 442;\n\n# ********************************************************************************************************\nSELECT * FROM users WHERE email LIKE '%nea.liikamaa@eletive.com%'; # 14022\nSELECT * FROM teams WHERE id = 429;\nselect * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);\nselect * from activities where opportunity_id in (4340436,4353519);\n\nselect * from transcription where activity_id IN (25630961,25381771);\nselect * from generic_ai_prompts where subject_id IN (4353519);\n\nSELECT\n a.id as activity_id,\n a.opportunity_id,\n a.type as activity_type,\n a.language,\n CONCAT(a.title, a.description) AS mail_content,\n e.from AS mail_from,\n e.to AS mail_to,\n e.subject AS mail_subject,\n e.body AS mail_body,\n p.type as prompt_type,\n p.status as prompt_status,\n p.content AS prompt_content,\n a.actual_start_time as created_at\nFROM activities a\n LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL\n LEFT JOIN email_messages e ON a.id = e.activity_id\nWHERE a.actual_start_time > '2024-01-01 00:00:00'\n AND a.opportunity_id IN (4353519)\n AND a.status IN ('completed', 'received', 'delivered')\n AND a.deleted_at IS NULL\n AND a.type NOT IN ('sms-inbound', 'sms-outbound')\nORDER BY a.opportunity_id ASC, a.id ASC;\n\nSELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293\nSELECT * FROM teams WHERE id = 442;\nSELECT * FROM crm_configurations WHERE id = 344;\nselect * from team_features where team_id = 442;\nselect * from groups where team_id = 442;\nselect * from playbooks where team_id = 442;\nselect * from playbook_categories where playbook_id = 1729;\nselect * from crm_fields where crm_configuration_id = 344 and id = 172024;\nSELECT * FROM crm_field_values WHERE crm_field_id = 172024;\nselect * from crm_layouts where crm_configuration_id = 344;\nselect * from playbook_layouts where playbook_id = 1729;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444\n\nselect s.*\n# , s.sent_at, u.name, a.*\nfrom activity_summary_logs s\ninner join activities a on a.id = s.activity_id\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 356\nand s.sent_at > date_sub(now(), interval 60 day)\norder by a.actual_end_time desc;\n\nselect * from activities a\n# inner join activity_summary_logs s on s.activity_id = a.id\nwhere a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)\n# and a.crm_provider_id is not null\n# and provider <> 'ringcentral'\nand status = 'completed'\norder by a.actual_end_time desc;\n\nselect * from teams order by id desc; # 17328, 32, 17830, integration-account@jiminny.com\nSELECT * FROM users;\nSELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active\nSELECT * FROM teams WHERE id = 260;\nselect * from team_settings where team_id = 260;\nselect * from crm_configurations where team_id = 260;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 356;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;\n\nselect * from accounts where crm_configuration_id = 221 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 221 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 221 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 221 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 221;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 221 order by id desc;\nselect * from stages where crm_configuration_id = 221 order by id desc;\n\nselect * from accounts where crm_configuration_id = 356 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 356 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 356 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 356 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 356;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 356 order by id desc;\nselect * from stages where crm_configuration_id = 356 order by id desc;\n\nselect * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)\nselect * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)\nselect * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4\nselect ce.* from calendars c\njoin users u on c.user_id = u.id\njoin calendar_events ce on c.id = ce.calendar_id\nwhere u.team_id = 260\nand (ce.start_time > '2025-02-21 00:00:00')\n;\n# calendar events 1207\n#\n\nselect * from opportunities where team_id = 260;\nSELECT * FROM crm_field_data WHERE object_id = 4696496;\n\nselect * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;\nselect * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')\n# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0\nand created_at > '2024-03-01 00:00:00'\norder by id desc; # 880 000, ringcentral, avaya\nSELECT * FROM participants WHERE activity_id = 26371744;\n\n# all activities 942 000 +\n# conference 7385 - scheduled 984 - external 343\n\nselect * from activities where id = 26321812;\nselect * from participants where activity_id = 26321812;\nselect * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);\nselect * from leads where id in (720428,689175,731546,645866,621037);\n\nselect * from users where id = 13841;\nselect * from opportunities where user_id = 9541;\nselect * from stages where id = 15900;\n\nselect * from accounts where\n# id IN (4160055,5053725,4965303,4896434)\nid in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)\n;\n\nselect * from activities where id = 26654935;\nSELECT * FROM opportunities WHERE id = 4803458;\n\nSELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;\nSELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time\nFROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);\n\nSELECT DISTINCT\n o.id, o.stage_id, s.name, a.title,\n a.*\nFROM activities a\n# INNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nINNER JOIN groups g ON u.group_id = g.id\nINNER JOIN opportunities o ON a.opportunity_id = o.id\nINNER JOIN stages s ON o.stage_id = s.id\nWHERE\n a.crm_configuration_id = 356\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 13841\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')\n AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')\n\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n )\n )\n AND (\n# s.id = 15900\n s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')\n OR s.uuid IS NULL -- Include records without opportunity stage\n )\n\nORDER BY a.actual_end_time DESC;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, willsc@leadforensics.com\nSELECT * FROM users WHERE team_id = 190;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 190\nand sa.provider = 'hubspot';\n\nselect * from role_user where user_id = 8474;\n\nselect * from crm_configurations where provider = 'bullhorn';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;\nSELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;\n\nSELECT * FROM opportunities WHERE id = 4732493;\nselect * from activities where opportunity_id = 4732493;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 443; # 358, 14315, andrea.romano@correrenaturale.com\nSELECT * FROM opportunities WHERE team_id = 443;\n\nSELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id\nFROM activities AS a\nJOIN stages AS s ON a.stage_id = s.id\nJOIN users AS u ON u.id = a.user_id\nJOIN teams AS t ON t.id = s.team_id\nWHERE u.team_id <> s.team_id and t.id > 135;\n\n\nSELECT\n crm_configuration_id,\n crm_provider_id,\n COUNT(*) as duplicate_count,\n GROUP_CONCAT(id) as stage_ids,\n GROUP_CONCAT(name) as stage_names\nFROM stages\nGROUP BY crm_configuration_id, crm_provider_id\nHAVING COUNT(*) > 1\nORDER BY duplicate_count DESC;\n\nselect * from stages where id IN (14898,14907);\n\nselect * from business_processes;\n\nSELECT *\nFROM crm_configurations\nWHERE team_id IN (\n SELECT team_id\n FROM crm_configurations\n GROUP BY team_id\n HAVING COUNT(*) > 1\n)\nORDER BY team_id;\n\nSELECT *\nFROM teams\nWHERE crm_id IN (\n SELECT crm_id\n FROM teams\n GROUP BY crm_id\n HAVING COUNT(*) > 1\n)\nORDER BY crm_id;\n\n# ***************************************************************************\nselect * from crm_configurations where provider = 'integration-app';\nSELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 andrea.romano@correrenaturale.com\nselect * from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect * from team_features where team_id = 358;\nselect * from activity_summary_logs;\n\nselect * from teams where id = 406;\n\n# ************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, srv.salesforce@sportfive.com\nselect * from activities where crm_configuration_id = 202 order by actual_end_time desc;\n\nSELECT * FROM users where id = 14637;\nSELECT * FROM teams where id = 267;\nSELECT * FROM groups where id = 1118;\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 202\n AND status IN ('completed', 'failed')\n AND recording_state != 'stopped'\n AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n AND (is_private = 0 OR user_id = 14637)\n AND (\n (\n actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n ) OR (\n actual_start_time IS NULL\n AND type IN ('sms-outbound', 'sms-inbound')\n AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tracks\n WHERE\n tracks.activity_id = activities.id\n AND tracks.type IN ('audio', 'video')\n )\nORDER BY actual_end_time DESC;\n\nSELECT DISTINCT\n a.*\nFROM activities a\nINNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nWHERE\n a.crm_configuration_id = 202\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 14637\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND a.user_id = 14637\n )\n )\n\nORDER BY a.actual_end_time DESC\n;\n\nSELECT DISTINCT a.*\nFROM activities a\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams t ON u.team_id = t.id\n# INNER JOIN tracks tr ON a.id = tr.activity_id\n# INNER JOIN groups g ON u.group_id = g.id\nWHERE 1=1\n AND t.id = 267\n# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND tr.type NOT IN ('audio', 'video')\n AND (\n a.is_private = 0\n OR a.user_id = 14637\n )\n AND (\n (a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')\n OR (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'\n )\n )\n# and NOT EXISTS (\n# SELECT 1\n# FROM tracks t\n# WHERE t.activity_id = a.id\n# AND t.type IN ('audio', 'video')\n# )\n\nORDER BY a.actual_end_time DESC;\n\nSELECT * FROM tracks WHERE activity_id = 26485995;\n\nselect a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 202\n# and a.is_internal = 0\nand (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type IN (\"softphone\",\"softphone-inbound\",\"conference\",\"sms-inbound\")\nand a.status IN ('completed', 'failed')\n# and a.external_id is not null\norder by a.actual_end_time desc;\n\nselect * from activities a where a.crm_configuration_id = 202\nand a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'\n# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_field_data WHERE crm_field_id = 98809;\n\nselect * from users where status = 1 AND timezone = 'MDT';\n\nselect * from opportunities where id = 3769814;\nselect * from deal_risks where opportunity_id = 3769814;\n\nselect cp.* from crm_profiles cp\njoin users u on cp.user_id = u.id\njoin crm_configurations crm on cp.crm_configuration_id = crm.id\nwhere crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';\n\nselect * from crm_fields where id = 154575;\n\nselect * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';\nSELECT * FROM teams WHERE id = 176; # crm 148\nselect * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nselect * from crm_fields cf\njoin crm_configurations crm on crm.id = cf.crm_configuration_id\nwhere crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');\n\n# *********************************************************************************************\nSELECT * FROM users WHERE id IN (15415, 15418);\nSELECT * FROM groups WHERE id IN (1805,1806);\nSELECT * FROM playbooks WHERE id = 1860;\nSELECT * FROM playbook_categories WHERE id = 38634;\nSELECT * FROM crm_fields WHERE id = 189962;\n\nSELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 raza.gilani@vuelio.com\n\nSELECT * FROM crm_profiles WHERE user_id = 15415;\nSELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';\n\nselect * from sidekick_settings where team_id = 472;\n\nSELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418\nSELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415\n\n# *********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, salesforce-integrations@teamtailor.com\nselect * from crm_configurations where id = 218;\nSELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765\nSELECT * FROM users WHERE id IN (13232, 13230);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n0057R00000EPL5HQAX Inez Ekblad\n\n1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur\n\nSELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);\n\n############################################################################################\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id IN (94491,94493,94498);\nSELECT * FROM users WHERE id = 13658;\nSELECT * FROM teams WHERE id = 109;\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, katy.holden@strengthscope.comk\nSELECT * FROM stages WHERE crm_configuration_id = 390;\nselect * from business_processes where team_id = 481 and crm_configuration_id = 390;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 481\nand sa.provider = 'salesforce';\n\n\nSELECT * FROM users WHERE id = 15780; # team 462\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 462\nand sa.provider = 'hubspot';\n\n\nselect * from teams where id = 495;\nSELECT * FROM users WHERE id = 15794;\nselect * from social_accounts where sociable_id = 15794;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752\nSELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794\nSELECT * FROM activities WHERE crm_configuration_id = 407\nand status = 'completed' and type = 'conference'\norder by id desc;\n\nselect ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id\njoin permission_role pr on pr.role_id = ru.role_id\n join permissions p on p.id = pr.permission_id\nwhere team_id = 495 and p.name IN ('dial');\n\nselect * from permission_role;\n\nselect * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;\nSELECT * FROM activities WHERE id = 29512773;\nSELECT * FROM activities WHERE id IN (29042721,28991325,29002874);\n\nSELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 407\n# and a.id IN (29042721,28991325,29002874);\n\nSELECT * FROM users WHERE id = 15794;\nSELECT * FROM users WHERE team_id = 495;\nSELECT * FROM social_accounts WHERE sociable_id = 15794;\nSELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';\nSELECT * FROM contacts WHERE team_id = 495;\nSELECT * FROM leads WHERE team_id = 495;\nSELECT * FROM accounts WHERE team_id = 495;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 407;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 407;\nSELECT * FROM crm_configurations WHERE id = 407;\nSELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'\nand user_id IS NOT NULL and is_closed = 1 and is_won = 1;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103\nSELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064\nSELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');\n\n# *********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 325\nand sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085\nSELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733\nSELECT * FROM activity_summary_logs where activity_id = 28719733;\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444\nSELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';\nSELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630\nselect * from activities where crm_configuration_id = 356 and lead_id = 841732;\n\nSELECT * from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 356;\n\nselect * from activities where crm_configuration_id = 356\nand actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'\norder by id desc;\n\nselect * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;\nselect * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\n\nselect * from team_features where team_id = 260;\nselect * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;\n\nselect * from crm_fields;\nselect * from crm_layout_entities;\n\nSELECT * FROM teams WHERE name LIKE '%Optable%';\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id in (94491,94493,94498);\n\nselect * from teams where crm_id IS NULL;\n\nSELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;\n\n# *************************************************************************************************\nselect * from team_domains where team_id = 399;\nSELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207\n\nselect * from calendar_events where id = 5163781;\nSELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896\nSELECT * FROM participants WHERE activity_id = 29443896;\nselect * from contacts where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\nselect * from leads where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\n\nselect * from activities where user_id = 14937 order by created_at ;\n\nselect * from users where id = 14937;\n\nselect * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';\nselect * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';\n\nselect * from activities a join participants p on a.id = p.activity_id\nwhere crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';\n\n# *************************************************************************************************\nSELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';\nSELECT * FROM opportunities WHERE team_id = 379 order by id desc;\nSELECT * FROM teams WHERE id = 379;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379 and sociable_id = 13852\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE id = 307;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 307;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307\n and id IN (144750,144855,145158,155227);\n\nSELECT * FROM activities;\n\n\nselect * from activities\nwhere created_at > '2025-07-01 00:00:00'\n# and created_at < '2025-08-01 00:00:00'\nand type not in ('email-outbound', 'email-inbound')\nand account_id is null\nand contact_id is null\nand lead_id is null\nand opportunity_id is not null\n;\nSELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);\nSELECT * FROM crm_configurations WHERE id in (335,301,200);\n\nselect * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';\n\nSELECT * FROM teams WHERE name LIKE '%Resights%';\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nselect * from teams where id IN (442);\n\nselect * from activities\nwhere crm_configuration_id = 177\nand provider = 'amazon-connect'\n order by id desc;\n# and source <> 'gong';\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nSELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;\n\n\nselect * from crm_configurations where store_transcript = 1;\nSELECT * FROM teams WHERE id IN (80);\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 277\nand sa.provider = 'salesforce';\n\nselect * from activities where crm_configuration_id = 213 and account_id = 2511502;\n\nselect * from crm_configurations where id = 213;\n\nSELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604\nSELECT * FROM participants WHERE activity_id = 33981604;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 431\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223\nselect * from activity_summary_logs where activity_id = 33997223;\nselect * from activity_notes where activity_id = 33997223;\n\n# ***********************************\nSELECT * FROM teams WHERE name LIKE '%Abode%';\n\n\nselect * from features;\nselect * from teams t\nwhere t.status = 'active'\nand id NOT IN (select team_id from team_features where feature_id = 9)\n;\n\n\nselect * from playbook_layouts where playbook_id = 1725;\nSELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473\nselect * from teams where id = 318;\nselect * from crm_configurations where team_id = 318;\nselect * from playbooks where team_id = 318;\nSELECT * FROM crm_layouts where crm_configuration_id = 381;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;\nSELECT * FROM crm_fields WHERE id IN (192938,192936,192939);\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;\nSELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);\n\nSELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533\n\n\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nSELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);\n\nSELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124\nselect * from crm_layouts where crm_configuration_id = 281;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nselect * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);\nselect * from opportunities where crm_configuration_id = 281;\n\nSELECT * FROM activities WHERE id IN (34211315, 34130075);\nSELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);\n\nselect cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd\njoin crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id\njoin crm_fields cf on cle.crm_field_id = cf.id\nwhere cf.deleted_at IS NOT NULL\nGROUP BY cle.id, cf.id;\n\nselect * from crm_layouts where id IN (355);\nselect u.email, t.crm_id, t.* from teams t\njoin users u on u.id = t.owner_id\nwhere crm_id IN (97);\n\nSELECT * FROM crm_fields WHERE id = 96492;\n\nselect * from permissions;\nselect * from permission_role where permission_id = 247;\nselect * from roles;\n\nselect * from migrations;\n# *****************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('291e3c21-11cc-4728-aee7-6e4bedf86d72') = uuid; # 34262174\nSELECT * FROM crm_configurations WHERE id = 301;\nSELECT * FROM teams WHERE id = 343;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from participants where activity_id = 34262174;\n\nselect * from contacts where crm_configuration_id = 301 and id = 6976326;\nselect * from accounts where crm_configuration_id = 301 and id IN (4647626, 4815829); # 30761335403\n\nselect * from activity_summary_logs where activity_id = 34262174;\n\nselect * from users where status = 1 AND timezone = 'EST';\n\n# ****************************************************************************\nSELECT * FROM users WHERE id = 13869;\nSELECT * FROM crm_configurations WHERE id = 320;\nSELECT * FROM teams WHERE id = 401;\n\nSELECT * FROM activities WHERE uuid_to_bin('2228c16f-10be-48d5-90d4-67385219dc01') = uuid; # 29670601\n\nSELECT * FROM accounts WHERE id = 7761483;\nSELECT * FROM opportunities WHERE id = 6051814;\n\nSELECT * FROM teams WHERE name LIKE '%Seedlegals%';\n\n;select * from opportunities where updated_at > '2025-10-11' AND crm_provider_id = '34713761166';\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 177;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 577;\nSELECT * FROM crm_fields WHERE id IN (68458,68459,68480,68497,68524,68530,68554,68618,68662,68781,68810,68898,68981,69049,97467);\n\nSELECT t.id, crm.id, t.name, crm.sync_objects, crm.provider, crm.last_synced_at FROM crm_configurations crm join teams t on t.crm_id = crm.id\nwhere t.status = 'active' AND crm.provider = 'hubspot' AND crm.last_synced_at < '2025-10-22 00:00:00';\n\nSELECT * FROM activities WHERE uuid_to_bin('fa09449f-cba9-496a-b8f3-865cd3c72351') = uuid;\nSELECT * FROM crm_configurations where id = 184;\nSELECT * FROM teams WHERE id = 246;\nSELECT * FROM social_accounts WHERE sociable_id = 9259 and provider = 'hubspot';\n\nSELECT * FROM users WHERE email LIKE '%rhian.old@bud.co.uk%'; # 17700\nSELECT * FROM teams WHERE id = 551;\n\nSELECT * FROM crm_configurations WHERE id = 471;\nSELECT * FROM activities WHERE crm_configuration_id = 471 and crm_provider_id IS NOT NULL;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 471;\nSELECT * FROM crm_fields WHERE id = 307260;\nSELECT * FROM crm_field_values WHERE crm_field_id = 307260;\n\nselect * from crm_layouts where crm_configuration_id = 471;\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1547;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1548;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 551 and sa.provider = 'hubspot';\n\nSELECT * FROM teams WHERE name LIKE '%$PCS%';\n\n# ********************************************************************************************************\nselect * from crm_configurations crm\njoin teams t on t.crm_id = crm.id\nwhere t.status = 'active'\nand crm.provider = 'hubspot';\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 crm_configurations where id = 331; # 416\nSELECT * FROM teams WHERE id = 416;\nSELECT * FROM opportunities WHERE team_id = 190;\n\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%';\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 190 and sa.provider = 'hubspot';\n\n\n\nSELECT * FROM teams WHERE name LIKE '%Rapaport%'; # 431, 337\nSELECT * FROM teams where id = 431;\nSELECT * FROM crm_configurations where team_id = 431;\nSELECT * FROM activity_providers where team_id = 431;\nSELECT * FROM activities where crm_configuration_id = 337 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 431 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%BiP%'; # 401, 320\nSELECT * FROM teams where id = 401;\nSELECT * FROM crm_configurations where team_id = 401;\nSELECT * FROM activity_providers where team_id = 401;\nSELECT * FROM activities where crm_configuration_id = 320 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 401 and sa.provider = 'salesforce';\n\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 307; # 379 - Story Terrace Inc , portalId: 3921157\nSELECT * FROM contacts WHERE team_id = 379 and updated_at > '2026-01-31 11:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 379 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; # 563 - LATUS Group (ad94d501-5d09-44fd-878f-ca3a9f8865c3) , portalId: 3904501\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 338; # 432 - Formalize , portalId: 9214205\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 432 and sa.provider = 'hubspot';\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 436; # 519 - Moxso , portalId: 25531989\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 96; # 119 - Nourish Care , portalId: 26617984\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 331; # 416 - The National College , portalId: 7213852\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 308; # 380 - Foodles , portalId: 7723616\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 379; # 471 - imat-uve , portalId: 9177354\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 465; # 545 - Spotler , portalId: 144759271\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 455; # 537 - indevis , portalId: 25666868\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 200; # 265 - Jobadder , portalId: 6426676\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 335; # 429 - Eletive , portalId: 6110563\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 363; # 456 - Global Group , portalId: 8901981\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 297; # 369 - Unbiased , portalId: 9229005\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 353; # 449 - Fuuse , portalId: 25781745\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 487; # 566 - Nimbus , portalId: 39982590\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 487;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1630;\nselect * from crm_fields where crm_configuration_id = 487 and\n(uuid_to_bin('4c6b2971-64d4-45b8-b377-427be758b5a5') = uuid or uuid_to_bin('59e368d8-65a0-4b77-b611-db37c99fbe68') = uuid);\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 420; # 506 - voiio , portalId: 145629154\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 479; # 558 - Momice , portalId: 535962\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 59; # 80 - Storyclash GmbH , portalId: 4268479\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 175; # 203 - Team iAM , portalId: 5534732\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 368; # 460 - OneTouch Health , portalId: 5534732183355\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\n\n\nselect * from users where id = 29643;\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM teams WHERE name LIKE '%Buynomics%'; # 462, 482, 14910\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\n# and description like '%The call focused on understanding Welch%'\norder by id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 462 and sa.provider = 'salesforce';\n\nselect * from contacts where crm_configuration_id = 482 and name = 'Cyndall Hill'; # 15504749\nselect * from contacts where id = 10891096; # 482\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\nand contact_id = 15504749\norder by id desc;\n\nselect * from activities where id = 36793003; # 96cc7bc1-8622-4d27-92f4-baf664fc1a56, 00UOf00000PDdOXMA1\nselect * from transcription where id = 7646782;\nselect * from ai_prompts where transcription_id = 7646782;\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7a8471a3-847e-4822-802b-ddf426bbc252') = uuid; # 37370018\nSELECT * FROM activity_summary_logs WHERE activity_id = 37370018;\nSELECT * FROM teams WHERE id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 555 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7c17b8aa-09df-4f85-a0f7-51f47afd712d') = uuid; # 37395250\nSELECT * FROM activities WHERE uuid_to_bin('14d60388-260d-494b-aa0d-63fdb1c78026') = uuid; # 37395250\n\nSELECT a.* FROM activities a JOIN crm_configurations c on c.id = a.crm_configuration_id\nwhere a.type IN ('softphone', 'softphone-outbound') and c.provider = 'hubspot'\nand a.provider NOT IN ('hubspot')\n# and a.provider IN ('salesloft')\n# and c.id NOT IN (70)\n# and a.duration > 30\n# and actual_start_time > '2026-02-05 00:00:00'\norder by a.id desc;\n\nSELECT * FROM activities WHERE id = 37549787;\nSELECT * FROM crm_profiles WHERE user_id = 17613;\n\nSELECT * FROM crm_configurations WHERE id = 70;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 93 and sa.provider = 'hubspot';\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations WHERE id = 373; # KPSBremen.de 465 # - no social account\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 465 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 494;\n\nSELECT * FROM teams WHERE name LIKE '%splose%'; # 572, 495, 18708\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 572 and sa.provider = 'pipedrive';\n\nselect * from opportunities where team_id = 572\n# and name like '%Onebright%'\n# and is_closed = 1 and is_won = 0\n order by id desc;\n\n\nselect * from users where deleted_at is null and status = 2;\n\nselect * from contacts where id = 17900517;\nselect * from accounts where id = 10109838;\nselect * from opportunities where id = 6955880;\n\nselect * from opportunity_contacts where opportunity_id = 6955880;\nselect * from opportunity_contacts where contact_id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nSELECT * FROM activities WHERE uuid_to_bin('adcb8331-5988-4353-834e-383a355abba2') = uuid; # 38056424, crm 104659682404\nselect * from teams where id = 456;\nSELECT * FROM crm_configurations WHERE id = 363;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 456 and sa.provider = 'hubspot';\n\nselect * from crm_layouts where crm_configuration_id = 363;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (1203, 1204, 1635);\nSELECT * FROM crm_fields WHERE id IN (181536, 181538, 213455);\n\nSELECT * FROM teams WHERE name LIKE '%Electric%'; # 342, 272, 12767\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and name like 'NORTHUMBRIA POL%'; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 order by remotely_created_at asc; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and updated_at > '2026-01-01 00:00:00';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 272 and object_type = 'opportunity';\nSELECT * FROM crm_field_values WHERE crm_field_id = 127164;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\n\nSELECT * FROM teams WHERE id = 472;\nSELECT * FROM crm_configurations WHERE id = 380;\nselect * from activities where id = 38285673; # 38285673\nSELECT * FROM users WHERE id = 16942;\nSELECT * FROM groups WHERE id = 1964;\nSELECT * FROM playbooks WHERE id = 2033;\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 499; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1678;\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\n\nSELECT * FROM activities WHERE uuid_to_bin('96b1261f-2357-49f9-ab38-23ce12008ea0') = uuid;\n\nselect * from contacts c\nwhere c.crm_configuration_id = 370 order by c.updated_at desc;\n\nSELECT * FROM participants where activity_id = 38833541;\nSELECT * FROM participants where activity_id = 39216301;\nSELECT * FROM activity_summary_logs where activity_id = 39216301;\nSELECT * FROM activities WHERE uuid_to_bin('c7d99fbe-1fb1-41f2-8f4d-52e2bf70e1e9') = uuid; # 38833541, crm 478116564181\nSELECT * FROM activities WHERE uuid_to_bin('2e6ff4d3-9faa-447a-a8c1-9acde4d885ae') = uuid; # 39216301, crm 480171536586\nselect * from crm_profiles where crm_configuration_id = 319 and crm_provider_id = 525785080;\nselect * from opportunities where crm_configuration_id = 319 and crm_provider_id = 410150124747;\nselect * from accounts where crm_configuration_id = 319 and crm_provider_id = 47150650569;\nselect * from contacts where crm_configuration_id = 319 and crm_provider_id IN ('665587441856', '742723347700');\n# owner 13236 525785080\n# contact 1 16779180 665587441856 - activity - Alex Howes alex@supportroom.com created 2026-01-26\n# contact 2 19247563 742723347700 - ash@supportroom.com 2026-03-24\n# company 4176133 47150650569\n# deal 7100953 410150124747\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 400 and sa.provider = 'hubspot';\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556; # owner: 18101, crm: 477\nselect * from crm_configurations where id = 477;\nSELECT * FROM users WHERE id = 18101;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'integration-app';\n\nselect * from opportunities where id = 7594349;\nselect * from opportunity_stages where opportunity_id = 7594349 order by created_at desc;\nselect * from business_processes where id = 6024;\nselect * from business_process_stages where stage_id = 16352;\nselect * from business_process_stages where business_process_id = 6024;\nselect * from stages where team_id = 459;\nselect * from teams where id = 459;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 459 and sa.provider = 'hubspot';\n\nSELECT os.stage_id, s.crm_provider_id, s.name, COUNT(*) as cnt\nFROM opportunity_stages os\nJOIN stages s ON s.id = os.stage_id\nWHERE os.opportunity_id = 7594349\nGROUP BY os.stage_id, s.crm_provider_id, s.name\nORDER BY cnt DESC;\n\nSELECT s.id, s.crm_provider_id, s.name, s.team_id, s.crm_configuration_id\nFROM stages s\nJOIN business_process_stages bps ON bps.stage_id = s.id\nWHERE bps.business_process_id = 6024\nAND s.crm_provider_id = 'contractsent';\n\nselect * from stages where id IN (16352,20612,18281,7344,16378,16309,5036,15223,14535,6293,12098,11607)\n\nSELECT * FROM teams WHERE name LIKE '%Pulsar Group%'; # 472, 380, 15138, raza.gilani@vuelio.com\nselect * from playbooks where team_id = 472; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 2288;\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 380;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 472 and sa.provider = 'salesforce';\n\nselect * from activities where id = 58081273;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2;\n\nSELECT * FROM users WHERE name LIKE '%Neil Hoyle%'; # 17651\nSELECT * FROM social_accounts WHERE sociable_id = 17651;\n\nSELECT * FROM activities WHERE uuid_to_bin('975c6830-7d49-4c1e-b2e9-ac80c10a738a') = uuid;\nSELECT * FROM opportunities WHERE id IN (7842553, 6211727);\nSELECT * FROM contacts WHERE id IN (10202724, 6211727);\nSELECT * FROM opportunity_stages WHERE opportunity_id = 7842553;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 519 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 436;\nselect * from crm_profiles where crm_configuration_id = 436; # 76091797 -> 16612\n\nselect * from contact_roles where contact_id = 10202724;\n\nselect * from stages where team_id = 519; # 18778\n18775\n\nSELECT\n id,\n crm_provider_id,\n stage_id,\n is_closed,\n is_won,\n stage_updated_at,\n updated_at\nFROM opportunities\nWHERE id IN (6211727, 7842553);\n\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id = 6211727 AND contact_id = 10202724;\n\nSELECT id, name, stage_id, is_closed, is_won, updated_at, remotely_created_at\nFROM opportunities\nWHERE account_id = 8179134\nORDER BY updated_at DESC;\n\n\nselect * from text_relays where created_at > '2026-01-01';\nAND id IN (691, 692);\n\nselect * from teams;\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = a.user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nSELECT DISTINCT u.id, u.email, u.name, u.team_id, t.name as team_name,\n t.twilio_sms_sid, t.twilio_messaging_sid\nFROM users u\nINNER JOIN teams t ON u.team_id = t.id\nWHERE (t.twilio_sms_sid IS NOT NULL OR t.twilio_messaging_sid IS NOT NULL)\nAND u.status = 1\nORDER BY t.name, u.email;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 187 and sa.provider = 'salesforce';\n\nselect * from activities where id = 31264367;\nselect * from contacts where id = 6331639;\nselect * from accounts where id = 4156632;\nselect * from opportunities where id = 4843610;\n# update `activities` set `account_id` = 4156632, `contact_id` = 6331639, `opportunity_id` = 4843610,\n# `stage_id` = 13273, `activities`.`updated_at` = 2026-05-22 07:16:17 where `id` = 31264367)\"\n\nselect * from text_relays where created_at > '2026-05-01';\n\nselect * from activities order by id desc;\n\nselect * from users where name like '%Subra%';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('04a9cfad-2c87-4453-9e72-20aeb78ccf8d') = uuid;\nselect * from teams where id = 555;\nselect * from stages where team_id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 100 and sa.provider = 'hubspot';\n\nselect id, is_closed, is_won, stage_updated_at, crm_provider_id, stage_id, probability,\nclose_date, forecast_category, deleted_at, created_at, remotely_created_at, updated_at\nfrom opportunities where team_id = 555 and stage_id = 20616 order by updated_at desc limit 10;\n\nselect id, crm_provider_id, name, probability, updated_at, deleted_at\nfrom stages\nwhere id = 20616;","depth":4,"on_screen":true,"value":"SELECT * FROM team_features where team_id = 1;\n\nSELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922\nSELECT * FROM users WHERE team_id = 340; # 12015\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 340\nand sa.provider = 'salesforce';\n# and sa.provider = 'salesloft';\n\nselect * from crm_fields where crm_configuration_id = 270 and object_type = 'event';\n# 125558 - Event Type - Event_Type__c\n# 125552 - Event Status - Event_Status__c\n\nSELECT * FROM sidekick_settings WHERE team_id = 340;\n\nSELECT * FROM crm_field_values WHERE crm_field_id in (125552);\n\nselect * from activities where crm_configuration_id = 270\nand type = 'conference' and crm_provider_id IS NOT NULL\nand actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;\n\nSELECT * FROM activities WHERE id = 20871677;\nSELECT * FROM crm_field_data WHERE activity_id = 20871677;\n\nselect * from crm_layouts where crm_configuration_id = 270;\nselect * from crm_layout_entities where crm_layout_id in (886,887);\n\nSELECT * FROM crm_configurations WHERE id = 270;\n\nselect * from playbooks where team_id = 340; # 1514\nselect * from groups where team_id = 340;\nSELECT * FROM crm_fields WHERE id IN (125393, 125401);\n\nselect g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g\njoin playbooks p on g.playbook_id = p.id\njoin crm_fields f on p.activity_field_id = f.id\nwhere g.team_id = 340;\n\nSELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716\nselect * from crm_field_data where object_id = 20448716;\n\nselect * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008\nselect * from opportunities where team_id = 343;\nselect * from opportunities where team_id = 343 and crm_provider_id = '18099102526';\nselect * from opportunities where team_id = 343 and account_id = 945217482;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from accounts where team_id = 343 order by name asc;\n\nselect * from stages where crm_configuration_id = 273 and type = 'opportunity';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143\nSELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;\nSELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';\nSELECT * FROM activities WHERE id = 20717903;\n\nselect * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 353\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, l.atkinson@mwbsolutions.co.uk\nSELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;\n# id: 20940638, user: 12022, contact: 5305871\nSELECT * FROM activity_summary_logs WHERE activity_id = 20940638;\nselect * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 345\nand sa.provider = 'hubspot';\n\nselect * from users where team_id = 345 and id = 12022;\nSELECT * FROM crm_profiles WHERE user_id = 12022;\nSELECT * FROM participants WHERE activity_id = 20940638;\nSELECT * FROM users u\nJOIN crm_profiles cp ON u.id = cp.user_id\nWHERE u.team_id = 345;\n\nselect * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871\n\nselect * from team_features where team_id = 345;\nSELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197\nSELECT * FROM participants WHERE activity_id = 20897406;\n\n\n\nSELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912\nSELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';\n\n\nSELECT * FROM activities WHERE id = 20946641;\nSELECT * FROM crm_profiles WHERE user_id = 10211;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, triger@lunio.ai\nSELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';\nselect * from stages where crm_configuration_id = 97 and type = 'opportunity';\nselect * from opportunities where team_id = 120;\n\n\nselect * from crm_configurations crm join teams t on crm.id = t.crm_id\nwhere 1=1\nAND t.current_billing_plan IS NOT NULL\nAND crm.auto_sync_activity = 0\nand crm.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,james.lewendon@exclaimer.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 270\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956\nSELECT * FROM crm_profiles WHERE user_id = 11446;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, alex.chikly@cygnetise.com\nselect * from playbooks where team_id = 372;\nselect * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340\nSELECT * FROM crm_field_values WHERE crm_field_id = 141340;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 372\nand sa.provider = 'salesforce';\n\nselect * from crm_profiles where crm_configuration_id = 300;\nSELECT * FROM crm_configurations WHERE team_id = 372;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,mfa@planday.com\nSELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756\nselect * from crm_field_data where object_id = 3207756;\nSELECT * FROM crm_fields WHERE id = 111834;\n\nselect f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value\nFROM crm_fields f\nJOIN crm_field_data fd ON f.id = fd.crm_field_id\nWHERE f.crm_configuration_id = 242\nAND f.object_type = 'opportunity'\nAND fd.object_id IN (3207756)\nORDER BY fd.object_id, fd.updated_at;\n\nSELECT * FROM crm_configurations WHERE auto_connect = 1;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,salesforce-admin@tourlane.com\nselect * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id\nwhere g.team_id = 187;\n\nselect * from `groups` where team_id = 187;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 187\nand sa.provider = 'salesforce';\n\n# Destination - 98870 - Destination__c\n# Stage - 79014 - StageName\n# Land Arrangement - 98856 - Land_Arrangement__c\n# Flight - 98848 - Flight__c\n# Last activity date - 98812 - LastActivityDate\n# Last modified date - 98809 - LastModifiedDate\n# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c\n# next call - 98864 - Next_Call__c\n\nselect * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\nselect * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';\nselect * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;\nselect * from activities where opportunity_id = 3538248;\n\nSELECT * FROM crm_profiles WHERE user_id = 8150;\n\nselect * from deal_risks where opportunity_id = 3538248;\n\nselect * from teams where crm_id IS NULL;\n\nSELECT opp.id AS opportunity_id,\n u.group_id AS group_id,\n MAX(\n CASE\n WHEN a.type IN (\"sms-inbound\", \"sms-outbound\") THEN a.created_at\n ELSE a.actual_end_time\n END) as last_date\nFROM opportunities opp\nleft join activities a on a.opportunity_id = opp.id\ninner join users u on opp.user_id = u.id\nwhere opp.user_id IN (9951)\n\nAND opp.is_closed = 0\nand a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL\ngroup by opp.id;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,polly.morphew@cybsafe.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 301;\nSELECT * FROM contacts WHERE id = 6612363;\nSELECT * FROM accounts WHERE id = 4235676;\nSELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;\nselect * from opportunity_stages where opportunity_id = 4503759;\n# SELECT * FROM opportunities WHERE id = 4569937;\n\nselect * from activities where crm_configuration_id = 301;\nSELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370\nSELECT * FROM participants WHERE activity_id = 26330370;\n\nSELECT * FROM teams WHERE id = 375;\nselect * from playbooks where team_id = 375;\n\nselect * from stages where crm_configuration_id = 301 and type = 'opportunity';\n\nselect * from teams;\nselect * from contact_roles;\n\nSELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';\n\nselect * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;\n\nSELECT * FROM crm_field_data WHERE object_id = 3771706;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'\nand crm_provider_id LIKE \"%traffic_light%\";\nSELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);\n\nSELECT fd.* FROM opportunities o\nJOIN crm_field_data fd ON o.id = fd.object_id\nWHERE o.team_id = 343\n# and o.user_id IS NOT NULL\nand fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)\nand fd.value != ''\norder by value desc\n# group by o.id\n;\n\nSELECT * FROM opportunities WHERE id = 3769843;\n\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, salesforce-admin@tourlane.com\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,aswini.mishra@fundingcircle.com\nSELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839\n\n\nSELECT * FROM opportunities WHERE id = 3855992;\n\nSELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988\n\nSELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';\n\nselect * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507\nSELECT * FROM crm_field_data WHERE object_id = 5874411;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379\nand sa.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, nikhil.kumar@mention-me.com\nSELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\nSELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793\nselect * from generic_ai_prompts where subject_id = 3537793;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, triger@lunio.ai\nSELECT * FROM crm_configurations WHERE id = 97;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 97;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;\nSELECT * FROM crm_fields WHERE id = 32682;\n\nselect cfd.value, o.* from opportunities o\njoin crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682\nwhere team_id = 120\nand cfd.value != ''\n;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 120\nand sa.provider = 'salesforce';\n\nselect * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';\nSELECT * FROM crm_field_data WHERE object_id = 2313439;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 410;\nSELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';\nselect * from scorecards where team_id = 410;\nselect * from scorecard_rules;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, aswini.mishra@fundingcircle.com\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\njoin users u on o.user_id = u.id\nwhere a.crm_configuration_id = 177 and a.type LIKE '%email-out%'\n# and a.actual_end_time > '2024-12-16 00:00:00'\n# and o.remotely_created_at > '2024-12-01 00:00:00'\n# and u.group_id = 1014\nand u.id = 9021\norder by a.id desc;\nSELECT * FROM opportunities WHERE id in (3981384,4017346);\nSELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);\n\nselect * from users where id = 9021;\nselect * from inboxes where user_id = 9021;\n\nselect * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';\n\nselect * from email_messages where team_id = 220\nand orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'\nand subject LIKE '%Personal%'\n# and 'from' = 'credit@fundingcircle.com'\n;\n\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\nwhere a.user_id = 9021 and a.type LIKE '%email-out%'\nand a.actual_end_time > '2024-12-18 00:00:00'\nand o.user_id IS NOT NULL\nand o.remotely_created_at > '2024-12-01 00:00:00'\norder by a.id desc;\n\nSELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;\nselect * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;\n\nselect * from team_settings where name IN ('useCloseDate');\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, jfarrell@hurree.co\nSELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 104\nand sa.provider = 'hubspot';\n\nselect * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'\nselect * from teams where crm_id IS NULL;\n\nselect t.name as 'team', u.name as 'owner', u.email, u.phone\nfrom teams t\njoin activity_providers ap on t.id = ap.team_id\njoin users u on t.owner_id = u.id\nwhere 1=1\n and t.status = 'active'\n and ap.is_enabled = 1\n# and u.status = 1\n and ap.provider = 'ms-teams';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nSELECT * FROM teams WHERE id = 442; # 14293\nselect * from users where team_id = 442;\nselect * from social_accounts sa where sa.sociable_id = 14293;\nselect * from invitations where team_id = 442;\n\n# ********************************************************************************************************\nSELECT * FROM users WHERE email LIKE '%nea.liikamaa@eletive.com%'; # 14022\nSELECT * FROM teams WHERE id = 429;\nselect * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);\nselect * from activities where opportunity_id in (4340436,4353519);\n\nselect * from transcription where activity_id IN (25630961,25381771);\nselect * from generic_ai_prompts where subject_id IN (4353519);\n\nSELECT\n a.id as activity_id,\n a.opportunity_id,\n a.type as activity_type,\n a.language,\n CONCAT(a.title, a.description) AS mail_content,\n e.from AS mail_from,\n e.to AS mail_to,\n e.subject AS mail_subject,\n e.body AS mail_body,\n p.type as prompt_type,\n p.status as prompt_status,\n p.content AS prompt_content,\n a.actual_start_time as created_at\nFROM activities a\n LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL\n LEFT JOIN email_messages e ON a.id = e.activity_id\nWHERE a.actual_start_time > '2024-01-01 00:00:00'\n AND a.opportunity_id IN (4353519)\n AND a.status IN ('completed', 'received', 'delivered')\n AND a.deleted_at IS NULL\n AND a.type NOT IN ('sms-inbound', 'sms-outbound')\nORDER BY a.opportunity_id ASC, a.id ASC;\n\nSELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293\nSELECT * FROM teams WHERE id = 442;\nSELECT * FROM crm_configurations WHERE id = 344;\nselect * from team_features where team_id = 442;\nselect * from groups where team_id = 442;\nselect * from playbooks where team_id = 442;\nselect * from playbook_categories where playbook_id = 1729;\nselect * from crm_fields where crm_configuration_id = 344 and id = 172024;\nSELECT * FROM crm_field_values WHERE crm_field_id = 172024;\nselect * from crm_layouts where crm_configuration_id = 344;\nselect * from playbook_layouts where playbook_id = 1729;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444\n\nselect s.*\n# , s.sent_at, u.name, a.*\nfrom activity_summary_logs s\ninner join activities a on a.id = s.activity_id\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 356\nand s.sent_at > date_sub(now(), interval 60 day)\norder by a.actual_end_time desc;\n\nselect * from activities a\n# inner join activity_summary_logs s on s.activity_id = a.id\nwhere a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)\n# and a.crm_provider_id is not null\n# and provider <> 'ringcentral'\nand status = 'completed'\norder by a.actual_end_time desc;\n\nselect * from teams order by id desc; # 17328, 32, 17830, integration-account@jiminny.com\nSELECT * FROM users;\nSELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active\nSELECT * FROM teams WHERE id = 260;\nselect * from team_settings where team_id = 260;\nselect * from crm_configurations where team_id = 260;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 356;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;\n\nselect * from accounts where crm_configuration_id = 221 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 221 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 221 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 221 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 221;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 221 order by id desc;\nselect * from stages where crm_configuration_id = 221 order by id desc;\n\nselect * from accounts where crm_configuration_id = 356 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 356 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 356 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 356 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 356;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 356 order by id desc;\nselect * from stages where crm_configuration_id = 356 order by id desc;\n\nselect * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)\nselect * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)\nselect * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4\nselect ce.* from calendars c\njoin users u on c.user_id = u.id\njoin calendar_events ce on c.id = ce.calendar_id\nwhere u.team_id = 260\nand (ce.start_time > '2025-02-21 00:00:00')\n;\n# calendar events 1207\n#\n\nselect * from opportunities where team_id = 260;\nSELECT * FROM crm_field_data WHERE object_id = 4696496;\n\nselect * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;\nselect * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')\n# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0\nand created_at > '2024-03-01 00:00:00'\norder by id desc; # 880 000, ringcentral, avaya\nSELECT * FROM participants WHERE activity_id = 26371744;\n\n# all activities 942 000 +\n# conference 7385 - scheduled 984 - external 343\n\nselect * from activities where id = 26321812;\nselect * from participants where activity_id = 26321812;\nselect * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);\nselect * from leads where id in (720428,689175,731546,645866,621037);\n\nselect * from users where id = 13841;\nselect * from opportunities where user_id = 9541;\nselect * from stages where id = 15900;\n\nselect * from accounts where\n# id IN (4160055,5053725,4965303,4896434)\nid in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)\n;\n\nselect * from activities where id = 26654935;\nSELECT * FROM opportunities WHERE id = 4803458;\n\nSELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;\nSELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time\nFROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);\n\nSELECT DISTINCT\n o.id, o.stage_id, s.name, a.title,\n a.*\nFROM activities a\n# INNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nINNER JOIN groups g ON u.group_id = g.id\nINNER JOIN opportunities o ON a.opportunity_id = o.id\nINNER JOIN stages s ON o.stage_id = s.id\nWHERE\n a.crm_configuration_id = 356\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 13841\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')\n AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')\n\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n )\n )\n AND (\n# s.id = 15900\n s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')\n OR s.uuid IS NULL -- Include records without opportunity stage\n )\n\nORDER BY a.actual_end_time DESC;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, willsc@leadforensics.com\nSELECT * FROM users WHERE team_id = 190;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 190\nand sa.provider = 'hubspot';\n\nselect * from role_user where user_id = 8474;\n\nselect * from crm_configurations where provider = 'bullhorn';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;\nSELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;\n\nSELECT * FROM opportunities WHERE id = 4732493;\nselect * from activities where opportunity_id = 4732493;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 443; # 358, 14315, andrea.romano@correrenaturale.com\nSELECT * FROM opportunities WHERE team_id = 443;\n\nSELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id\nFROM activities AS a\nJOIN stages AS s ON a.stage_id = s.id\nJOIN users AS u ON u.id = a.user_id\nJOIN teams AS t ON t.id = s.team_id\nWHERE u.team_id <> s.team_id and t.id > 135;\n\n\nSELECT\n crm_configuration_id,\n crm_provider_id,\n COUNT(*) as duplicate_count,\n GROUP_CONCAT(id) as stage_ids,\n GROUP_CONCAT(name) as stage_names\nFROM stages\nGROUP BY crm_configuration_id, crm_provider_id\nHAVING COUNT(*) > 1\nORDER BY duplicate_count DESC;\n\nselect * from stages where id IN (14898,14907);\n\nselect * from business_processes;\n\nSELECT *\nFROM crm_configurations\nWHERE team_id IN (\n SELECT team_id\n FROM crm_configurations\n GROUP BY team_id\n HAVING COUNT(*) > 1\n)\nORDER BY team_id;\n\nSELECT *\nFROM teams\nWHERE crm_id IN (\n SELECT crm_id\n FROM teams\n GROUP BY crm_id\n HAVING COUNT(*) > 1\n)\nORDER BY crm_id;\n\n# ***************************************************************************\nselect * from crm_configurations where provider = 'integration-app';\nSELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 andrea.romano@correrenaturale.com\nselect * from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect * from team_features where team_id = 358;\nselect * from activity_summary_logs;\n\nselect * from teams where id = 406;\n\n# ************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, srv.salesforce@sportfive.com\nselect * from activities where crm_configuration_id = 202 order by actual_end_time desc;\n\nSELECT * FROM users where id = 14637;\nSELECT * FROM teams where id = 267;\nSELECT * FROM groups where id = 1118;\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 202\n AND status IN ('completed', 'failed')\n AND recording_state != 'stopped'\n AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n AND (is_private = 0 OR user_id = 14637)\n AND (\n (\n actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n ) OR (\n actual_start_time IS NULL\n AND type IN ('sms-outbound', 'sms-inbound')\n AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tracks\n WHERE\n tracks.activity_id = activities.id\n AND tracks.type IN ('audio', 'video')\n )\nORDER BY actual_end_time DESC;\n\nSELECT DISTINCT\n a.*\nFROM activities a\nINNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nWHERE\n a.crm_configuration_id = 202\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 14637\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND a.user_id = 14637\n )\n )\n\nORDER BY a.actual_end_time DESC\n;\n\nSELECT DISTINCT a.*\nFROM activities a\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams t ON u.team_id = t.id\n# INNER JOIN tracks tr ON a.id = tr.activity_id\n# INNER JOIN groups g ON u.group_id = g.id\nWHERE 1=1\n AND t.id = 267\n# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND tr.type NOT IN ('audio', 'video')\n AND (\n a.is_private = 0\n OR a.user_id = 14637\n )\n AND (\n (a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')\n OR (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'\n )\n )\n# and NOT EXISTS (\n# SELECT 1\n# FROM tracks t\n# WHERE t.activity_id = a.id\n# AND t.type IN ('audio', 'video')\n# )\n\nORDER BY a.actual_end_time DESC;\n\nSELECT * FROM tracks WHERE activity_id = 26485995;\n\nselect a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 202\n# and a.is_internal = 0\nand (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type IN (\"softphone\",\"softphone-inbound\",\"conference\",\"sms-inbound\")\nand a.status IN ('completed', 'failed')\n# and a.external_id is not null\norder by a.actual_end_time desc;\n\nselect * from activities a where a.crm_configuration_id = 202\nand a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'\n# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_field_data WHERE crm_field_id = 98809;\n\nselect * from users where status = 1 AND timezone = 'MDT';\n\nselect * from opportunities where id = 3769814;\nselect * from deal_risks where opportunity_id = 3769814;\n\nselect cp.* from crm_profiles cp\njoin users u on cp.user_id = u.id\njoin crm_configurations crm on cp.crm_configuration_id = crm.id\nwhere crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';\n\nselect * from crm_fields where id = 154575;\n\nselect * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';\nSELECT * FROM teams WHERE id = 176; # crm 148\nselect * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nselect * from crm_fields cf\njoin crm_configurations crm on crm.id = cf.crm_configuration_id\nwhere crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');\n\n# *********************************************************************************************\nSELECT * FROM users WHERE id IN (15415, 15418);\nSELECT * FROM groups WHERE id IN (1805,1806);\nSELECT * FROM playbooks WHERE id = 1860;\nSELECT * FROM playbook_categories WHERE id = 38634;\nSELECT * FROM crm_fields WHERE id = 189962;\n\nSELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 raza.gilani@vuelio.com\n\nSELECT * FROM crm_profiles WHERE user_id = 15415;\nSELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';\n\nselect * from sidekick_settings where team_id = 472;\n\nSELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418\nSELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415\n\n# *********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, salesforce-integrations@teamtailor.com\nselect * from crm_configurations where id = 218;\nSELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765\nSELECT * FROM users WHERE id IN (13232, 13230);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n0057R00000EPL5HQAX Inez Ekblad\n\n1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur\n\nSELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);\n\n############################################################################################\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id IN (94491,94493,94498);\nSELECT * FROM users WHERE id = 13658;\nSELECT * FROM teams WHERE id = 109;\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, katy.holden@strengthscope.comk\nSELECT * FROM stages WHERE crm_configuration_id = 390;\nselect * from business_processes where team_id = 481 and crm_configuration_id = 390;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 481\nand sa.provider = 'salesforce';\n\n\nSELECT * FROM users WHERE id = 15780; # team 462\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 462\nand sa.provider = 'hubspot';\n\n\nselect * from teams where id = 495;\nSELECT * FROM users WHERE id = 15794;\nselect * from social_accounts where sociable_id = 15794;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752\nSELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794\nSELECT * FROM activities WHERE crm_configuration_id = 407\nand status = 'completed' and type = 'conference'\norder by id desc;\n\nselect ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id\njoin permission_role pr on pr.role_id = ru.role_id\n join permissions p on p.id = pr.permission_id\nwhere team_id = 495 and p.name IN ('dial');\n\nselect * from permission_role;\n\nselect * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;\nSELECT * FROM activities WHERE id = 29512773;\nSELECT * FROM activities WHERE id IN (29042721,28991325,29002874);\n\nSELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 407\n# and a.id IN (29042721,28991325,29002874);\n\nSELECT * FROM users WHERE id = 15794;\nSELECT * FROM users WHERE team_id = 495;\nSELECT * FROM social_accounts WHERE sociable_id = 15794;\nSELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';\nSELECT * FROM contacts WHERE team_id = 495;\nSELECT * FROM leads WHERE team_id = 495;\nSELECT * FROM accounts WHERE team_id = 495;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 407;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 407;\nSELECT * FROM crm_configurations WHERE id = 407;\nSELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'\nand user_id IS NOT NULL and is_closed = 1 and is_won = 1;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103\nSELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064\nSELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');\n\n# *********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 325\nand sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085\nSELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733\nSELECT * FROM activity_summary_logs where activity_id = 28719733;\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444\nSELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';\nSELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630\nselect * from activities where crm_configuration_id = 356 and lead_id = 841732;\n\nSELECT * from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 356;\n\nselect * from activities where crm_configuration_id = 356\nand actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'\norder by id desc;\n\nselect * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;\nselect * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\n\nselect * from team_features where team_id = 260;\nselect * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;\n\nselect * from crm_fields;\nselect * from crm_layout_entities;\n\nSELECT * FROM teams WHERE name LIKE '%Optable%';\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id in (94491,94493,94498);\n\nselect * from teams where crm_id IS NULL;\n\nSELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;\n\n# *************************************************************************************************\nselect * from team_domains where team_id = 399;\nSELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207\n\nselect * from calendar_events where id = 5163781;\nSELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896\nSELECT * FROM participants WHERE activity_id = 29443896;\nselect * from contacts where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\nselect * from leads where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\n\nselect * from activities where user_id = 14937 order by created_at ;\n\nselect * from users where id = 14937;\n\nselect * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';\nselect * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';\n\nselect * from activities a join participants p on a.id = p.activity_id\nwhere crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';\n\n# *************************************************************************************************\nSELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';\nSELECT * FROM opportunities WHERE team_id = 379 order by id desc;\nSELECT * FROM teams WHERE id = 379;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379 and sociable_id = 13852\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE id = 307;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 307;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307\n and id IN (144750,144855,145158,155227);\n\nSELECT * FROM activities;\n\n\nselect * from activities\nwhere created_at > '2025-07-01 00:00:00'\n# and created_at < '2025-08-01 00:00:00'\nand type not in ('email-outbound', 'email-inbound')\nand account_id is null\nand contact_id is null\nand lead_id is null\nand opportunity_id is not null\n;\nSELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);\nSELECT * FROM crm_configurations WHERE id in (335,301,200);\n\nselect * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';\n\nSELECT * FROM teams WHERE name LIKE '%Resights%';\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nselect * from teams where id IN (442);\n\nselect * from activities\nwhere crm_configuration_id = 177\nand provider = 'amazon-connect'\n order by id desc;\n# and source <> 'gong';\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nSELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;\n\n\nselect * from crm_configurations where store_transcript = 1;\nSELECT * FROM teams WHERE id IN (80);\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 277\nand sa.provider = 'salesforce';\n\nselect * from activities where crm_configuration_id = 213 and account_id = 2511502;\n\nselect * from crm_configurations where id = 213;\n\nSELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604\nSELECT * FROM participants WHERE activity_id = 33981604;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 431\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223\nselect * from activity_summary_logs where activity_id = 33997223;\nselect * from activity_notes where activity_id = 33997223;\n\n# ***********************************\nSELECT * FROM teams WHERE name LIKE '%Abode%';\n\n\nselect * from features;\nselect * from teams t\nwhere t.status = 'active'\nand id NOT IN (select team_id from team_features where feature_id = 9)\n;\n\n\nselect * from playbook_layouts where playbook_id = 1725;\nSELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473\nselect * from teams where id = 318;\nselect * from crm_configurations where team_id = 318;\nselect * from playbooks where team_id = 318;\nSELECT * FROM crm_layouts where crm_configuration_id = 381;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;\nSELECT * FROM crm_fields WHERE id IN (192938,192936,192939);\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;\nSELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);\n\nSELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533\n\n\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nSELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);\n\nSELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124\nselect * from crm_layouts where crm_configuration_id = 281;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nselect * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);\nselect * from opportunities where crm_configuration_id = 281;\n\nSELECT * FROM activities WHERE id IN (34211315, 34130075);\nSELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);\n\nselect cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd\njoin crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id\njoin crm_fields cf on cle.crm_field_id = cf.id\nwhere cf.deleted_at IS NOT NULL\nGROUP BY cle.id, cf.id;\n\nselect * from crm_layouts where id IN (355);\nselect u.email, t.crm_id, t.* from teams t\njoin users u on u.id = t.owner_id\nwhere crm_id IN (97);\n\nSELECT * FROM crm_fields WHERE id = 96492;\n\nselect * from permissions;\nselect * from permission_role where permission_id = 247;\nselect * from roles;\n\nselect * from migrations;\n# *****************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('291e3c21-11cc-4728-aee7-6e4bedf86d72') = uuid; # 34262174\nSELECT * FROM crm_configurations WHERE id = 301;\nSELECT * FROM teams WHERE id = 343;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from participants where activity_id = 34262174;\n\nselect * from contacts where crm_configuration_id = 301 and id = 6976326;\nselect * from accounts where crm_configuration_id = 301 and id IN (4647626, 4815829); # 30761335403\n\nselect * from activity_summary_logs where activity_id = 34262174;\n\nselect * from users where status = 1 AND timezone = 'EST';\n\n# ****************************************************************************\nSELECT * FROM users WHERE id = 13869;\nSELECT * FROM crm_configurations WHERE id = 320;\nSELECT * FROM teams WHERE id = 401;\n\nSELECT * FROM activities WHERE uuid_to_bin('2228c16f-10be-48d5-90d4-67385219dc01') = uuid; # 29670601\n\nSELECT * FROM accounts WHERE id = 7761483;\nSELECT * FROM opportunities WHERE id = 6051814;\n\nSELECT * FROM teams WHERE name LIKE '%Seedlegals%';\n\n;select * from opportunities where updated_at > '2025-10-11' AND crm_provider_id = '34713761166';\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 177;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 577;\nSELECT * FROM crm_fields WHERE id IN (68458,68459,68480,68497,68524,68530,68554,68618,68662,68781,68810,68898,68981,69049,97467);\n\nSELECT t.id, crm.id, t.name, crm.sync_objects, crm.provider, crm.last_synced_at FROM crm_configurations crm join teams t on t.crm_id = crm.id\nwhere t.status = 'active' AND crm.provider = 'hubspot' AND crm.last_synced_at < '2025-10-22 00:00:00';\n\nSELECT * FROM activities WHERE uuid_to_bin('fa09449f-cba9-496a-b8f3-865cd3c72351') = uuid;\nSELECT * FROM crm_configurations where id = 184;\nSELECT * FROM teams WHERE id = 246;\nSELECT * FROM social_accounts WHERE sociable_id = 9259 and provider = 'hubspot';\n\nSELECT * FROM users WHERE email LIKE '%rhian.old@bud.co.uk%'; # 17700\nSELECT * FROM teams WHERE id = 551;\n\nSELECT * FROM crm_configurations WHERE id = 471;\nSELECT * FROM activities WHERE crm_configuration_id = 471 and crm_provider_id IS NOT NULL;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 471;\nSELECT * FROM crm_fields WHERE id = 307260;\nSELECT * FROM crm_field_values WHERE crm_field_id = 307260;\n\nselect * from crm_layouts where crm_configuration_id = 471;\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1547;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1548;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 551 and sa.provider = 'hubspot';\n\nSELECT * FROM teams WHERE name LIKE '%$PCS%';\n\n# ********************************************************************************************************\nselect * from crm_configurations crm\njoin teams t on t.crm_id = crm.id\nwhere t.status = 'active'\nand crm.provider = 'hubspot';\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 crm_configurations where id = 331; # 416\nSELECT * FROM teams WHERE id = 416;\nSELECT * FROM opportunities WHERE team_id = 190;\n\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%';\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 190 and sa.provider = 'hubspot';\n\n\n\nSELECT * FROM teams WHERE name LIKE '%Rapaport%'; # 431, 337\nSELECT * FROM teams where id = 431;\nSELECT * FROM crm_configurations where team_id = 431;\nSELECT * FROM activity_providers where team_id = 431;\nSELECT * FROM activities where crm_configuration_id = 337 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 431 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%BiP%'; # 401, 320\nSELECT * FROM teams where id = 401;\nSELECT * FROM crm_configurations where team_id = 401;\nSELECT * FROM activity_providers where team_id = 401;\nSELECT * FROM activities where crm_configuration_id = 320 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 401 and sa.provider = 'salesforce';\n\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 307; # 379 - Story Terrace Inc , portalId: 3921157\nSELECT * FROM contacts WHERE team_id = 379 and updated_at > '2026-01-31 11:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 379 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; # 563 - LATUS Group (ad94d501-5d09-44fd-878f-ca3a9f8865c3) , portalId: 3904501\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 338; # 432 - Formalize , portalId: 9214205\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 432 and sa.provider = 'hubspot';\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 436; # 519 - Moxso , portalId: 25531989\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 96; # 119 - Nourish Care , portalId: 26617984\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 331; # 416 - The National College , portalId: 7213852\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 308; # 380 - Foodles , portalId: 7723616\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 379; # 471 - imat-uve , portalId: 9177354\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 465; # 545 - Spotler , portalId: 144759271\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 455; # 537 - indevis , portalId: 25666868\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 200; # 265 - Jobadder , portalId: 6426676\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 335; # 429 - Eletive , portalId: 6110563\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 363; # 456 - Global Group , portalId: 8901981\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 297; # 369 - Unbiased , portalId: 9229005\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 353; # 449 - Fuuse , portalId: 25781745\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 487; # 566 - Nimbus , portalId: 39982590\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 487;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1630;\nselect * from crm_fields where crm_configuration_id = 487 and\n(uuid_to_bin('4c6b2971-64d4-45b8-b377-427be758b5a5') = uuid or uuid_to_bin('59e368d8-65a0-4b77-b611-db37c99fbe68') = uuid);\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 420; # 506 - voiio , portalId: 145629154\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 479; # 558 - Momice , portalId: 535962\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 59; # 80 - Storyclash GmbH , portalId: 4268479\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 175; # 203 - Team iAM , portalId: 5534732\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 368; # 460 - OneTouch Health , portalId: 5534732183355\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\n\n\nselect * from users where id = 29643;\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM teams WHERE name LIKE '%Buynomics%'; # 462, 482, 14910\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\n# and description like '%The call focused on understanding Welch%'\norder by id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 462 and sa.provider = 'salesforce';\n\nselect * from contacts where crm_configuration_id = 482 and name = 'Cyndall Hill'; # 15504749\nselect * from contacts where id = 10891096; # 482\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\nand contact_id = 15504749\norder by id desc;\n\nselect * from activities where id = 36793003; # 96cc7bc1-8622-4d27-92f4-baf664fc1a56, 00UOf00000PDdOXMA1\nselect * from transcription where id = 7646782;\nselect * from ai_prompts where transcription_id = 7646782;\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7a8471a3-847e-4822-802b-ddf426bbc252') = uuid; # 37370018\nSELECT * FROM activity_summary_logs WHERE activity_id = 37370018;\nSELECT * FROM teams WHERE id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 555 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7c17b8aa-09df-4f85-a0f7-51f47afd712d') = uuid; # 37395250\nSELECT * FROM activities WHERE uuid_to_bin('14d60388-260d-494b-aa0d-63fdb1c78026') = uuid; # 37395250\n\nSELECT a.* FROM activities a JOIN crm_configurations c on c.id = a.crm_configuration_id\nwhere a.type IN ('softphone', 'softphone-outbound') and c.provider = 'hubspot'\nand a.provider NOT IN ('hubspot')\n# and a.provider IN ('salesloft')\n# and c.id NOT IN (70)\n# and a.duration > 30\n# and actual_start_time > '2026-02-05 00:00:00'\norder by a.id desc;\n\nSELECT * FROM activities WHERE id = 37549787;\nSELECT * FROM crm_profiles WHERE user_id = 17613;\n\nSELECT * FROM crm_configurations WHERE id = 70;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 93 and sa.provider = 'hubspot';\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations WHERE id = 373; # KPSBremen.de 465 # - no social account\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 465 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 494;\n\nSELECT * FROM teams WHERE name LIKE '%splose%'; # 572, 495, 18708\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 572 and sa.provider = 'pipedrive';\n\nselect * from opportunities where team_id = 572\n# and name like '%Onebright%'\n# and is_closed = 1 and is_won = 0\n order by id desc;\n\n\nselect * from users where deleted_at is null and status = 2;\n\nselect * from contacts where id = 17900517;\nselect * from accounts where id = 10109838;\nselect * from opportunities where id = 6955880;\n\nselect * from opportunity_contacts where opportunity_id = 6955880;\nselect * from opportunity_contacts where contact_id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nSELECT * FROM activities WHERE uuid_to_bin('adcb8331-5988-4353-834e-383a355abba2') = uuid; # 38056424, crm 104659682404\nselect * from teams where id = 456;\nSELECT * FROM crm_configurations WHERE id = 363;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 456 and sa.provider = 'hubspot';\n\nselect * from crm_layouts where crm_configuration_id = 363;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (1203, 1204, 1635);\nSELECT * FROM crm_fields WHERE id IN (181536, 181538, 213455);\n\nSELECT * FROM teams WHERE name LIKE '%Electric%'; # 342, 272, 12767\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and name like 'NORTHUMBRIA POL%'; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 order by remotely_created_at asc; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and updated_at > '2026-01-01 00:00:00';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 272 and object_type = 'opportunity';\nSELECT * FROM crm_field_values WHERE crm_field_id = 127164;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\n\nSELECT * FROM teams WHERE id = 472;\nSELECT * FROM crm_configurations WHERE id = 380;\nselect * from activities where id = 38285673; # 38285673\nSELECT * FROM users WHERE id = 16942;\nSELECT * FROM groups WHERE id = 1964;\nSELECT * FROM playbooks WHERE id = 2033;\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 499; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1678;\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\n\nSELECT * FROM activities WHERE uuid_to_bin('96b1261f-2357-49f9-ab38-23ce12008ea0') = uuid;\n\nselect * from contacts c\nwhere c.crm_configuration_id = 370 order by c.updated_at desc;\n\nSELECT * FROM participants where activity_id = 38833541;\nSELECT * FROM participants where activity_id = 39216301;\nSELECT * FROM activity_summary_logs where activity_id = 39216301;\nSELECT * FROM activities WHERE uuid_to_bin('c7d99fbe-1fb1-41f2-8f4d-52e2bf70e1e9') = uuid; # 38833541, crm 478116564181\nSELECT * FROM activities WHERE uuid_to_bin('2e6ff4d3-9faa-447a-a8c1-9acde4d885ae') = uuid; # 39216301, crm 480171536586\nselect * from crm_profiles where crm_configuration_id = 319 and crm_provider_id = 525785080;\nselect * from opportunities where crm_configuration_id = 319 and crm_provider_id = 410150124747;\nselect * from accounts where crm_configuration_id = 319 and crm_provider_id = 47150650569;\nselect * from contacts where crm_configuration_id = 319 and crm_provider_id IN ('665587441856', '742723347700');\n# owner 13236 525785080\n# contact 1 16779180 665587441856 - activity - Alex Howes alex@supportroom.com created 2026-01-26\n# contact 2 19247563 742723347700 - ash@supportroom.com 2026-03-24\n# company 4176133 47150650569\n# deal 7100953 410150124747\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 400 and sa.provider = 'hubspot';\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556; # owner: 18101, crm: 477\nselect * from crm_configurations where id = 477;\nSELECT * FROM users WHERE id = 18101;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'integration-app';\n\nselect * from opportunities where id = 7594349;\nselect * from opportunity_stages where opportunity_id = 7594349 order by created_at desc;\nselect * from business_processes where id = 6024;\nselect * from business_process_stages where stage_id = 16352;\nselect * from business_process_stages where business_process_id = 6024;\nselect * from stages where team_id = 459;\nselect * from teams where id = 459;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 459 and sa.provider = 'hubspot';\n\nSELECT os.stage_id, s.crm_provider_id, s.name, COUNT(*) as cnt\nFROM opportunity_stages os\nJOIN stages s ON s.id = os.stage_id\nWHERE os.opportunity_id = 7594349\nGROUP BY os.stage_id, s.crm_provider_id, s.name\nORDER BY cnt DESC;\n\nSELECT s.id, s.crm_provider_id, s.name, s.team_id, s.crm_configuration_id\nFROM stages s\nJOIN business_process_stages bps ON bps.stage_id = s.id\nWHERE bps.business_process_id = 6024\nAND s.crm_provider_id = 'contractsent';\n\nselect * from stages where id IN (16352,20612,18281,7344,16378,16309,5036,15223,14535,6293,12098,11607)\n\nSELECT * FROM teams WHERE name LIKE '%Pulsar Group%'; # 472, 380, 15138, raza.gilani@vuelio.com\nselect * from playbooks where team_id = 472; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 2288;\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 380;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 472 and sa.provider = 'salesforce';\n\nselect * from activities where id = 58081273;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2;\n\nSELECT * FROM users WHERE name LIKE '%Neil Hoyle%'; # 17651\nSELECT * FROM social_accounts WHERE sociable_id = 17651;\n\nSELECT * FROM activities WHERE uuid_to_bin('975c6830-7d49-4c1e-b2e9-ac80c10a738a') = uuid;\nSELECT * FROM opportunities WHERE id IN (7842553, 6211727);\nSELECT * FROM contacts WHERE id IN (10202724, 6211727);\nSELECT * FROM opportunity_stages WHERE opportunity_id = 7842553;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 519 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 436;\nselect * from crm_profiles where crm_configuration_id = 436; # 76091797 -> 16612\n\nselect * from contact_roles where contact_id = 10202724;\n\nselect * from stages where team_id = 519; # 18778\n18775\n\nSELECT\n id,\n crm_provider_id,\n stage_id,\n is_closed,\n is_won,\n stage_updated_at,\n updated_at\nFROM opportunities\nWHERE id IN (6211727, 7842553);\n\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id = 6211727 AND contact_id = 10202724;\n\nSELECT id, name, stage_id, is_closed, is_won, updated_at, remotely_created_at\nFROM opportunities\nWHERE account_id = 8179134\nORDER BY updated_at DESC;\n\n\nselect * from text_relays where created_at > '2026-01-01';\nAND id IN (691, 692);\n\nselect * from teams;\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = a.user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nSELECT DISTINCT u.id, u.email, u.name, u.team_id, t.name as team_name,\n t.twilio_sms_sid, t.twilio_messaging_sid\nFROM users u\nINNER JOIN teams t ON u.team_id = t.id\nWHERE (t.twilio_sms_sid IS NOT NULL OR t.twilio_messaging_sid IS NOT NULL)\nAND u.status = 1\nORDER BY t.name, u.email;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 187 and sa.provider = 'salesforce';\n\nselect * from activities where id = 31264367;\nselect * from contacts where id = 6331639;\nselect * from accounts where id = 4156632;\nselect * from opportunities where id = 4843610;\n# update `activities` set `account_id` = 4156632, `contact_id` = 6331639, `opportunity_id` = 4843610,\n# `stage_id` = 13273, `activities`.`updated_at` = 2026-05-22 07:16:17 where `id` = 31264367)\"\n\nselect * from text_relays where created_at > '2026-05-01';\n\nselect * from activities order by id desc;\n\nselect * from users where name like '%Subra%';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('04a9cfad-2c87-4453-9e72-20aeb78ccf8d') = uuid;\nselect * from teams where id = 555;\nselect * from stages where team_id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 100 and sa.provider = 'hubspot';\n\nselect id, is_closed, is_won, stage_updated_at, crm_provider_id, stage_id, probability,\nclose_date, forecast_category, deleted_at, created_at, remotely_created_at, updated_at\nfrom opportunities where team_id = 555 and stage_id = 20616 order by updated_at desc limit 10;\n\nselect id, crm_provider_id, name, probability, updated_at, deleted_at\nfrom stages\nwhere id = 20616;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7945821021870316922
|
-537191020546090905
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
1
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'ac...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
88400
|
3015
|
3
|
2026-05-28T17:01:45.225085+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987705225_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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":"Show Replace Field","depth":4,"bounds":{"left":0.11868351,"top":0.15482841,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.13131648,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"existingStages","depth":4,"bounds":{"left":0.14228724,"top":0.15403032,"width":0.05319149,"height":0.015961692},"on_screen":true,"value":"existingStages","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3061616431686199205
|
-8493296012604765244
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
rapstomEV faVsco,ls ~ProjectvViewCooc#12121 on JY-20963-fix-InWindowTO0У L7Inu Lo mey koulU ServiceTest ~© ServiceTest.phpHubspotClientinterface.ph© HubspotTokenManager.pt© PayloadBuilder.phpoKimol Cimosewhoo© ResponseNormalize.php© SyncFieidAction.phpCSWnCKealCohe wWwK© WebhookSyncBatchProce>D IntegrationAodlisteners> MetadataMigrationP oedriveSalesforcealeeldsOpportunityMatcher17.03.25 ilian16.04.25 IvanovGrahanGraham19.03.18 Graham2.04.18GrahamOpportunitySyncStrategyProspectSearchStrategysametite20.10.21 Graham© Cient.php2.10.25© DecorateActivity.php2.04.18GrahamG DeleteObjectsTrait.php16.04.25 Ivanov2.10.25©FieldDefinitions.php© PayloadBuilder.php2.10.25© Profile.php8.11.18©QueryBullder.php2.04.18oiahim©QueryHandler.php19.03.18 Graham©Queryiterator.php©QueryResults.php4.05.26© Service.php4.05.26© SyncBatchRedisService.pt4.05.26Traits4.05.26© BaseCllent.php4.05.264.05.26© Cached Crm Service Decorator2.10.252.10.2540524.05.26© CachedCrmServiceDecorator.php©MatchActivityCrmData.phpRecordSelector.php© Activity.php© Team,phpE IaravellogA HSJJocal (jiminny@localhost]# СОЛЬОС PHUA console (EU] x ( users (EU)A console (STAGING]Cascadeles Orcnnworeeoeionhomwwtino oooo+0.ResponseNormalize.phgHwwo.noocea.cokicontooensytsdorooe© PayloadBulider.phpexisiinastaad10.02.23 Vasilev24.01.25 Papazov386© CrmÊntity Repository.phpSe jiminny~• git show 51d7201d5b:app/Services/Cra/Hubspot/Service-php | grep -A 70 "function inportStages" | tail -25031 49 A29 X3 X109 A V"crn_provider_id' = $s('id*),Crmactivity ProviderintegrateCCnlACMiWMohCermcontcurationSettnosser©rmobiectsteso wer.ono© DefaultProspectSearchStrateC mallteloer.ondC) LavouMansoe onoC Opportun tvActvitwlatcheeeennortur wewn.CtestomedProspectCache.php17.03.25 ilianHe OrnenontCostrhSrond nhn4.05.26418420422427429447class Service extends BaseService inplements170601 A7 A149 X1 X33 21 A v 1707public function inportStages(Parray Styposuvoau, Potrine SpisspngStoreNone z null): 25ta99-1708mayruvoaniuoort veroorol oproseoiyioosel"crn_provider_1d => Spl'10"J170917101. Gpchisostennosidr*> nb_strinwidth(Spt'Label'], Start 0, (width: 150),=> BusinessProcess::TYPE_OPPORTUNITY,is_selectable'=> Sp['active"].1711171217131714171517161717Il A record type is really a clone of the business process, used to store which record ;US 1218// Create/update record type cloneSthis->config->recordTypes()->update0rCreateCCE171s"cra_provider _id' => Sp['id*],=17201, G"tean_id'=> Sthis->tean->id,"Is_selectable'→> nb_strimwidth($p['label'],start: 0,width: 158).→> $p['active'),'business_process_id* => $businessProcess->id 2? null,D):// Stages - fetch all existing stages upfront to avoid N+1 querieses = Sthis->config->stages()hrrashede->ahere('type', Stage: : TYPE_OPPORTUNITY)->0e10foreach (Sp['stages'] as SdealStage) (Ss = Responselornaze::nornobelen Stage Sdeal Stage)?/zx Exar ?Stege SexistingStage */SexistingStage = SpxistingStages->get($s['id'D):// Restore soft-deleted stages that are now active in HubSpot1€ (aydstinoStaoe)-strashedtà se sef'active'1) ?=1722$17241726÷17271728=172917301731E1732-17331734=1735=1736—1733- 1738=1735=1742—1742=1743=1744=17451746 v—1/40—174811!Thc AutoORDER BY t.name, u.email;SELECT * FROM teans WHERE nane LIKE "XTourlane%'; # 187, 209, 8154.SELECTCONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE-u.enail,sa.x,t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.tean_idWHERE u.tean_id = 187 and sa.provider = 'salesforce":The probability logic was identical in Nov 2025. Now let me check nornslizebealStage at that time:select * fron activities where id = 31264367:select * fron contacts where id = 6331639;select * fron accounts where id = 4156632;select * fron opportunities where id = 4843610;#Uodare300n8-4300-4"contact_id' = 6.#"stage_id' = 13273,"updated_at' = 2026-0S-22 07:16:select * fron text_relays where created_at > '2026-05-01':• git show Sfd728fdSb:app/Services/Crn/Hubspot/ResponseNormalize-php | grep -A 28 *normalizeDealStage)public static function normalizebealstagetarray lobject sdealstage): arrayAlso idenucal. Now let me check tne getdes LsPipe Lines cndpo int - this is what the Nov 2026 bux impori uscd to leich prpeline data. Was there aselect * fron actviates order oy 1d desc;select * fron users where nane uike "subrasapet tnesEndpgin, sh°2/dev/null || hesd -eSELECT * FROM opportunities HHERE wwid_to_bin('04a9cfad-2c87-4453-sselect * fron teans where 1d= 555%select * fron stages where tean_id = 555;SETSTMTCONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE:lhenasa.*,t.ouner_id FROM social_accounts saJOIN users u on u.id= sa.sociable_idJOIN teans t 1.n<->1: on t.id = u.tean.idWHERE u.tean_id = 100 and sa.provider = "hubspet":git log -oneline -diff-f{lter-M — app/Services/Crm/Hubspot/Service.php | head -20pestrr eelieted stages,restore in recreatedtestsAdd pereission Logging for HubspotTmaortBuspoessProressess- are cleaned up tron all CPMs but Salesforce.CRMs but Salesforce.6xI/ Upsert stage (updates soft-deleted records without restoring then)Sstage = Sthis->config->stages()->withTcashed() -›update0rCreate(ESone nnovidon dat es Cereaaellselect id, is_closed, is_won, stage_updated_at, crm_provider_id, stclose_date, forecast_category, deleted_at, created_at, renotely_crtfron opportunities where tean_id = 5SS and stage_id = 28616 order_*select id, crn.provider.id, nane, probability, updated.at, deleter!fron stageswhere id = 28616;O git Log —oneline —before="2825-11-19" - app/Services/Crn/Hubspot/Service.php | head -3(Run s= (Skip"tean. id"=> Sthis->team->id,=> nb_strimwidth(Ss('label'], (start: e,width: 50),Ask anything (XOL)Adhet• OytwodturlatmeXekwhi..h2 4 spao...
|
88398
|
NULL
|
NULL
|
NULL
|
|
88399
|
3014
|
1
|
2026-05-28T17:01:45.329050+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987705329_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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":"ServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9087091289234339902
|
-4023455704353896256
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:01:458 10Untitled +...
|
88396
|
NULL
|
NULL
|
NULL
|
|
88398
|
3015
|
2
|
2026-05-28T17:01:40.139986+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987700139_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomEV faVsco,ls ~ProjectvCooc#12121 on JY-2096 rapstomEV faVsco,ls ~ProjectvCooc#12121 on JY-20963-fix-InHubspotClientinterface.ph© HubspotTokenManager.pt© PayloadBuilder.phpoKimol Cimosewhoo© ResponseNormalize.phpCSeMCPono© SyncFieidAction.phpCSWnCKealCohe wWwK© WebhookSyncBatchProcewwswo..noocea.cokecontoensyicstocc.mexisiinastaad10.02.23 VasilevDaeosnszollisteners> MetadataMigrationP oedriveSalesforceafeldsOpportunityMatcherOpportunitySyncStrategyProspectSearchStrategysametite© Client.php© DecorateActivity.phpG DeleteObjectsTrait.php©FieldDefinitions.php© PayloadBuilder.php© Profile.php©QueryBullder.php©QueryHandler.php©Queryiterator.php©QueryResults.php© Service.php© SyncBatchRedisService.ptTraits© BaseCllent.phpCwastemoeene© CachedCrmServiceDecorator2.04.18Graham19.03.18 Graham2.04.18Grandy3.10.252.10.25CIU.LS20.10.21 Graham2lU.LS17.05.25 mlar2.10.25Ctahnath3.10.25cransmh19.03.18 Graham2.10.2520207 Graham70413Grahhm8.11182.04.18Graham1002 18CrahsmCrmactivity ProviderintegrateCCWCMWMoron4.05.264.05.264.05.26©rmobiectctesower.on© DefaultProspectSearchStrate4.05.264.05.26 IvanosC mallteloer.ond4.05.26 Ivanov3sindeProscectinteance.ond2.10.25C) LvoutMansoeono3.10.25C Opportun tvActvitwlatchee4.05.20© OpportunitySyncStrategyResProspectCache.phpHe OrnenontCostrhSrond nhn TO0У L7Inu Lo mey LoulalU ServiceTest ~© ServiceTest.php©MatchActivityCrmData.php© Activity.php© Team,phpE IaravellogA HSJJocal (jiminny@localhost]A console (STAGING]4 SF [iminny@localhost)# СОЛЬОС PHUA console (EU] x E users (EU)Cascadeles Orcnnworeeoeionhomwso1ino ooeoVo Show Sl-CD0=onNs/anuosoowserce.cho oroosh4ocondro+0.© CiosedDealStagesService.php© CrmÊntity Repository.phpclass Service extends BaseService inplements170601 47 A149 X1 X33 21 A V 1707public function importStages(Parray $types = null, Pstring SnissingStageName = null): 2Stage—1708foreach (Spipelines as Spipeline) €17091710Sstages = (1:1711I/ We create a business process to contain the pipeline, and store all stages against it.1713$p = ResponseNornalize: :normalizePipeline(Spipeline);1715// Create/update business process for this pipelinejousznessrrocess a suzsescontzc@ousanessrrocessesouodareurcreacecri provzoer 20 5 3010-1717-17183unisoseaeson=> mb_strimwidth(Sp['label'],saltor=> BusinessProcess::TYPE_OPPORTUNITY,=> Sp['active'].=1720= 1727172311724lunecorewor railyaclone onthe ous tness orocussy lustedotormnchneconeV 1726I/ Create/update record type cloneSthis->confio->necondTvoes@->undate0rCneatech=1727>> Spt'id'],=1729=17301731mheeninmidthSor"nbelen→> Spl'actáve'),busiiness anocessid' = ShussinessProcesse>d2% nuluE1732-1733=17351736Stages - fetch all existing stages upfront to avoid N+1 queriesexistingStages = Sthis->config->stages()-withTrashedO)->ahere('type', Stage:: TYPE_OPPORTUNITY)→get-›keyBy('crn_provider_id'):E1757-1738E1739=1742—1742=1743foreach (Sp['stages'] as $dealStage) ($s = ResponseNornalize: :normalizeDealStage(SdealStage);=1744=17451746 vThc AutoORDER BY t.nane, u.email;Se jiminny~031 49 A29 X3 X109 A VSELECT * FROM teans WHERE nane LIKE "XTourlane%'; # 187, 209, 8154.SELECTCONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE-u.enail,sa.x,t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.tean_idWHERE u.tean_id = 187 and sa.provider = 'salesforce":• git show Sfd728fdSb:app/Services/Crn/Hubspot/Service-php | grep -A 50 "function inportStages"public function ieportStages(zarray stypes e nutl, istring salssingStagclame = nutl): 75tageSaissingStage • null;try i use the HubSpot API client instead of the SOK crmPipelines() sethodce()->getClient ()->request('GET', Sendpoint):mmand di oreo.tat show Stozz roso:aoo/Servicos//cra/Hubspot/Service.pho areo an 78 "Tunction teoor Staoestart=5"emorouider d' n scrid"select * fron activities where id = 31264367:select * fron contacts where id = 6331639;select * fron accounts where id = 4156632;select * fron opportunities where id = 4843610;#Uodare0c0un8-4300-4"contact_id' = 6.#"stage_id' = 13273,"updated_at' = 2026-05-22 07:16:1, L: 8, 59%5.B ssl'probabatlty'1 100)select * fron text_relays where created_at > '2026-05-01':tihworobnsry loccwrklidcoatinotorowilet.mechockcrtreoensthcehthattmselect * fron actvales order oy 1d desca• qít show S187201d5b:app/Services/Crm/Hubspot/ResponseNormalize.php | grep -A 20 "norma Lizedea lStage)select * fron users where nane uike subras:public static function nornalizedealStagelarray|object sdeatstage): arraySELECT * FROM opportunities HHERE wwid_to_bin('04a9cfad-2c87-4453-swaesetowronas aoeselect * fron teans where 1d= 555%return.!select * fron stages where tean_id = 555;SETSTMT2 o acttdiplaytroer') ®),CONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE:lhenasa.*,t.ouner_id FROM social_accounts sa©at od mooelin cditoaiterl e aon/Services/Cmy/Hubsoot /Servi co,oho lhead =213T22yd0500 J7-20801 KestoreJOIN users u on u.id = sa.sociable_idrestore in recreatedJOIN teans t 1.n<->1: on t.id = u.tean.idWHERE u.tean_id = 100 and sa.provider = 'hubsRes":/кx @xac ?Stage SexistingStage */SexistingStage = SexistingStages->get(Ss['id'D);select id, is_closed, is_won, stage_updated_at, crm_provider_id, stclose_date, forecast_category, deleted_at, created_at, renotely_crtfron opportunities where tean_id = 5SS and stage_id = 28616 order_*select id, crn_provider_id, nane, probability, updated_at, deleterefron stageswhere id = 28616;"Dtt ctRs but Sples ore cleaned up fron all CRMs but Solesforce1/ Restore soft-deleted stages that are now active in HubSpotif (SexistingStage?->trashed() && $s('active']) €—174811!Ask anything (XOL)• OKwodturlasmeXekwiires2 4 spac...
|
NULL
|
5584569429245352126
|
NULL
|
visual_change
|
ocr
|
NULL
|
rapstomEV faVsco,ls ~ProjectvCooc#12121 on JY-2096 rapstomEV faVsco,ls ~ProjectvCooc#12121 on JY-20963-fix-InHubspotClientinterface.ph© HubspotTokenManager.pt© PayloadBuilder.phpoKimol Cimosewhoo© ResponseNormalize.phpCSeMCPono© SyncFieidAction.phpCSWnCKealCohe wWwK© WebhookSyncBatchProcewwswo..noocea.cokecontoensyicstocc.mexisiinastaad10.02.23 VasilevDaeosnszollisteners> MetadataMigrationP oedriveSalesforceafeldsOpportunityMatcherOpportunitySyncStrategyProspectSearchStrategysametite© Client.php© DecorateActivity.phpG DeleteObjectsTrait.php©FieldDefinitions.php© PayloadBuilder.php© Profile.php©QueryBullder.php©QueryHandler.php©Queryiterator.php©QueryResults.php© Service.php© SyncBatchRedisService.ptTraits© BaseCllent.phpCwastemoeene© CachedCrmServiceDecorator2.04.18Graham19.03.18 Graham2.04.18Grandy3.10.252.10.25CIU.LS20.10.21 Graham2lU.LS17.05.25 mlar2.10.25Ctahnath3.10.25cransmh19.03.18 Graham2.10.2520207 Graham70413Grahhm8.11182.04.18Graham1002 18CrahsmCrmactivity ProviderintegrateCCWCMWMoron4.05.264.05.264.05.26©rmobiectctesower.on© DefaultProspectSearchStrate4.05.264.05.26 IvanosC mallteloer.ond4.05.26 Ivanov3sindeProscectinteance.ond2.10.25C) LvoutMansoeono3.10.25C Opportun tvActvitwlatchee4.05.20© OpportunitySyncStrategyResProspectCache.phpHe OrnenontCostrhSrond nhn TO0У L7Inu Lo mey LoulalU ServiceTest ~© ServiceTest.php©MatchActivityCrmData.php© Activity.php© Team,phpE IaravellogA HSJJocal (jiminny@localhost]A console (STAGING]4 SF [iminny@localhost)# СОЛЬОС PHUA console (EU] x E users (EU)Cascadeles Orcnnworeeoeionhomwso1ino ooeoVo Show Sl-CD0=onNs/anuosoowserce.cho oroosh4ocondro+0.© CiosedDealStagesService.php© CrmÊntity Repository.phpclass Service extends BaseService inplements170601 47 A149 X1 X33 21 A V 1707public function importStages(Parray $types = null, Pstring SnissingStageName = null): 2Stage—1708foreach (Spipelines as Spipeline) €17091710Sstages = (1:1711I/ We create a business process to contain the pipeline, and store all stages against it.1713$p = ResponseNornalize: :normalizePipeline(Spipeline);1715// Create/update business process for this pipelinejousznessrrocess a suzsescontzc@ousanessrrocessesouodareurcreacecri provzoer 20 5 3010-1717-17183unisoseaeson=> mb_strimwidth(Sp['label'],saltor=> BusinessProcess::TYPE_OPPORTUNITY,=> Sp['active'].=1720= 1727172311724lunecorewor railyaclone onthe ous tness orocussy lustedotormnchneconeV 1726I/ Create/update record type cloneSthis->confio->necondTvoes@->undate0rCneatech=1727>> Spt'id'],=1729=17301731mheeninmidthSor"nbelen→> Spl'actáve'),busiiness anocessid' = ShussinessProcesse>d2% nuluE1732-1733=17351736Stages - fetch all existing stages upfront to avoid N+1 queriesexistingStages = Sthis->config->stages()-withTrashedO)->ahere('type', Stage:: TYPE_OPPORTUNITY)→get-›keyBy('crn_provider_id'):E1757-1738E1739=1742—1742=1743foreach (Sp['stages'] as $dealStage) ($s = ResponseNornalize: :normalizeDealStage(SdealStage);=1744=17451746 vThc AutoORDER BY t.nane, u.email;Se jiminny~031 49 A29 X3 X109 A VSELECT * FROM teans WHERE nane LIKE "XTourlane%'; # 187, 209, 8154.SELECTCONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE-u.enail,sa.x,t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.tean_idWHERE u.tean_id = 187 and sa.provider = 'salesforce":• git show Sfd728fdSb:app/Services/Crn/Hubspot/Service-php | grep -A 50 "function inportStages"public function ieportStages(zarray stypes e nutl, istring salssingStagclame = nutl): 75tageSaissingStage • null;try i use the HubSpot API client instead of the SOK crmPipelines() sethodce()->getClient ()->request('GET', Sendpoint):mmand di oreo.tat show Stozz roso:aoo/Servicos//cra/Hubspot/Service.pho areo an 78 "Tunction teoor Staoestart=5"emorouider d' n scrid"select * fron activities where id = 31264367:select * fron contacts where id = 6331639;select * fron accounts where id = 4156632;select * fron opportunities where id = 4843610;#Uodare0c0un8-4300-4"contact_id' = 6.#"stage_id' = 13273,"updated_at' = 2026-05-22 07:16:1, L: 8, 59%5.B ssl'probabatlty'1 100)select * fron text_relays where created_at > '2026-05-01':tihworobnsry loccwrklidcoatinotorowilet.mechockcrtreoensthcehthattmselect * fron actvales order oy 1d desca• qít show S187201d5b:app/Services/Crm/Hubspot/ResponseNormalize.php | grep -A 20 "norma Lizedea lStage)select * fron users where nane uike subras:public static function nornalizedealStagelarray|object sdeatstage): arraySELECT * FROM opportunities HHERE wwid_to_bin('04a9cfad-2c87-4453-swaesetowronas aoeselect * fron teans where 1d= 555%return.!select * fron stages where tean_id = 555;SETSTMT2 o acttdiplaytroer') ®),CONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE:lhenasa.*,t.ouner_id FROM social_accounts sa©at od mooelin cditoaiterl e aon/Services/Cmy/Hubsoot /Servi co,oho lhead =213T22yd0500 J7-20801 KestoreJOIN users u on u.id = sa.sociable_idrestore in recreatedJOIN teans t 1.n<->1: on t.id = u.tean.idWHERE u.tean_id = 100 and sa.provider = 'hubsRes":/кx @xac ?Stage SexistingStage */SexistingStage = SexistingStages->get(Ss['id'D);select id, is_closed, is_won, stage_updated_at, crm_provider_id, stclose_date, forecast_category, deleted_at, created_at, renotely_crtfron opportunities where tean_id = 5SS and stage_id = 28616 order_*select id, crn_provider_id, nane, probability, updated_at, deleterefron stageswhere id = 28616;"Dtt ctRs but Sples ore cleaned up fron all CRMs but Solesforce1/ Restore soft-deleted stages that are now active in HubSpotif (SexistingStage?->trashed() && $s('active']) €—174811!Ask anything (XOL)• OKwodturlasmeXekwiires2 4 spac...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
88397
|
3015
|
1
|
2026-05-28T17:01:34.272919+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987694272_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"height":0.025538707},"on_screen":true,"is_enabled":true,"is_selected":false,"is_expanded":false}]...
|
9087091289234339902
|
-4023455704353896256
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
rapstomEV faVsco,ls ~#12121 on JY-20963-fix-InProjectvCoocWindowThu 28 May 20:01:33U ServiceTest ~© ServiceTest.php© DeleteObjects Trait.php©MatchActivityCrmData.phpE custom.logE Iaravellog4 SF [iminny@localhost)CascadeHubspotClientinterface.phRecordSelector.php© Activity.php© Team,phpA HSJJocal (jiminny@localhost]# Conboc PhoA console (EU] x ( users (EU)les Orcnnworeebeeionhomwstotino ooso+0.© HubspotTokenManager.pt© PayloadBuilder.phpA console (STAGING]oKimol Cimosewhoowwswo..noocea.cokecontoensyicstocc.m© PayloadBulider.php© CiosedDealStagesService.php© CrmÊntity Repository.phpThc AutoSe jiminny~© ResponseNormalize.php1705ORDER BY t.nane, u.email;• git show Std720fdSb:app/Services/Crn/Hubspot/Service-php | grep -A 50 "function inportStages"1706031 49 A29 X3 X109 A VCSeMCPonoexistinastaadecwpubtic tunction amportstages (rarray Stypes = nutt, (string saissingstagenane = nutt): 15tage© SyncFieidAction.php10.02.23 Vasilevclass Service extends BaseService inplements01 47 A149 X1 233 21 A V 1707SELECT * FROM teans WHERE nane LIKE "XTourlane%'; # 187, 209, 8154.SaissingStage = nult;CSWnCKealCohe wWwK24.01.25 Papazovpublic function inportStages(Parray Stypes = null, Pstring SnissingStageName = null): 2Stage-1708SEEiNT© WebhookSyncBatchProce1709CONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE-try(// Use the HubSpot APT client instead of the SDK crapipelines() nethod391// Use the HubSpot API client instead of the SDK crmPipelines() nethod1710u.enail,onse - 5this-scltent-sgetinstance()-agetCLient ()-эrequest('GET', Sendpoint):listeners3.10.25Sendpoint = self::getDealsPipelinesEndpoint():sa.x,> Metadata13.10.25 Nikolov3931711SpipelinesResponse = Sthis->client->getInstance()-›getCLient()->request( method: 'GET", Sen 1712t.ouner_id FROM social_accounts saMigration13.10.25 NikolovSpipelines = SpipelinesResponse-›data-›results;JOIN users u on u.id = sa.sociable_idP oedrive10.09.25 flianSalesforce24.04.18 Graham395} catch (RequestException|BadRequest Sexception) (сллou pexсeaeoth1715JOIN teans t 1.nc->1: on t.id = u.tean_idoshowololo100owresr@/huosco@/servce.onoortounceloncoorsoor=WHERE u.tean_id = 187 and sa.provider = 'salesforce":afelds24.04.18 Graham*crn_provider_id' = $s('1d'),OpportunityMatcher19.03.18 GrahamOpportunitySyncStrategy-1717select * fron activities where id = 31264367:121025 NikoloProspectSearchStrategy2.04.18Graham399foreach (Spipelines as Spipeline) (-1718Sstages = [J:-1728select * fron contacts where id = 6331639;ab strimwioth(sst' Larteatatssr uabel:l: 8, 191.select * fron accounts where id = 4156632;sametite19.03.18 Grahamselect * fron opportunities where id = 4843610;© Cient.php2.04.18#Uodare0c0un8-4300-4"contact_id' = 6.© DecorateActivity.php3.10.25403$p = ResponseNornalize: :nornalizePipeline(Spipeline);#"stage_id' = 13273,"updated_at' = 2026-05-22 07:16:G DeleteObjectsTrait.php2.10.25The probablTity locic was identical in Nov 2025. Now let me check norma &izedea Stace at that timel©FieldDefinitions.php// Create/update business process for this pipelineselect * fron text_relays nhere created_at > '2026-05-01':© PayloadBuilder.php20.10.21 GrahamSbusinessProcess = Sthis->config->businessProcesses()->update0rCreateCE© Profile.php2.10.25©QueryBullder.php17.03.25 ilian407select * fron actvales order oy 1d desca408)1, ttoshowoturoortoo/terces/criuos.cou//Resoonscwor.lozze.ohoor80=7n0m0701215.000publie statie function normslizeDealStage(array lobject sdealStage): array©QueryHandler.php1604.75 Ivanov409= his-xean->1d.©Querylterator.php2.10.25=> mb_strimwidth(Sp( 'label'],Start 8rwiodt58)©QueryResults.php2.04.18Graham•> BusinessProcess: :TYPE_OPPORTUNITY,© Service.php3.10.25→> $p['active'].© SyncBatchRedisService.pt2.04.18GrahamTraits19.03.18 Graham© BaseCllent.php04418© BaseService.php2.10.2511 eretoro eate ie oeatye clone of the business proces, vsed fo stere mnich recend vn// Create/update record type clone© Cached Crm Service DecoratorSthisosconfiaesnecondTvnes(l-sundatehofceatech© CrmActivityProviderintegrateCCnlACMiWMoh1, 0©rmobiectctesower.on© DefaultProspectSearchStrateC Emal telcerond3sindeProscectinteance.ondC) LavouMansoe onoeraewdthssolahe"is_selectable'>> Sp['actáve'),"business_process_id' = $businessProcess->id ?? nulz,2.04.18Graham100218 GrabanC Opportun tvActvitwlatchee© OpportunitySyncStrategyResProspectCache.phpHe OrnenontCostrhSrond nhn426428430432D):(/ Stages - fetch all existing stages upfront to avoid N+1 queriesexistingStages = Sthis->config->stages()-withTrashedO-›ahere('type', Stage:: TYPE_OPPORTUNITY)→getO-›keyBy('crn_provider_id'):=1727=172917301731E152E1757-1738E1739=1742—1742=1743=1744=17451746 v— 1/40—174811!select * fron users where nane uike subras:SELECT * FROM opportunities HHERE wuid_to_bin('04a9cfad-2c87-4453-sselect * fron teans where 1d= 555%select * fron stages where tean_id = 555;SETSTMTlhenasa.*,t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teans t 1.n<->1: on t.id = u.tean.idWHERE u.tean_id = 100 and sa.provider = 'hubsRes":(sdatal'"displayOrder') 27 0),→(bool)" (Sdatal"active"CONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE:Also identical. Now let me check the oetdes &Pioe Tines Endoo int - this is what the Noy 2025 ouimoort uerd to tetch o oeline data, Was there &different endpoint being used at that time?• g1t 109 - onellne -Bll - spp/Services/Cra/tubspot/Service-php / xargs -20) sh -c git, ahon2/dev/null | head -SCommand cin nasoselect id, is_closed, is_won, stage_updated_at, crn_provider_id, atclose_date, forecast_category, deleted_at, created_at, renotely_crtfron opportunities where tean_id = 5SS and stage_id = 28616 order_*select id, crn_provider_id, nane, probability, updated_at, deleterefron stageswhere id = 28616;O git log —oneline -diff-filtersM - app/Services/Crm/Hubspot/Service-php | head -20Run st= (SkipAsk anything (XOL)Adhetforeach (Sof'stages'] as SdealStaae) (• OKwodturlasmeXekwiires2 4 space...
|
88395
|
NULL
|
NULL
|
NULL
|
|
88396
|
3014
|
0
|
2026-05-28T17:01:34.624837+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987694624_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
HomeDMsActivityFilesLater..•More+Slack> 0(ah]Fi HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:01:348 10Untitled +...
|
NULL
|
5275823833128158783
|
NULL
|
click
|
ocr
|
NULL
|
HomeDMsActivityFilesLater..•More+Slack> 0(ah]Fi HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:01:348 10Untitled +...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
88394
|
NULL
|
0
|
2026-05-28T17:01:24.226811+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987684226_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
HomeDMsActivityFilesLater..•More+Slack> 0(ah]Fi HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:01:248 10Untitled +...
|
NULL
|
7339909769605185208
|
NULL
|
click
|
ocr
|
NULL
|
HomeDMsActivityFilesLater..•More+Slack> 0(ah]Fi HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:01:248 10Untitled +...
|
88391
|
NULL
|
NULL
|
NULL
|
|
88393
|
NULL
|
0
|
2026-05-28T17:01:21.405229+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987681405_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8569994499127135030
|
-3986301007428073024
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-lProinet vHubspotClientinterface.ph© HubspotTokenManager.pt© PayloadBuilder.phpoKimo CimosewhooResponseNormalize.phoCSeMCPono© SyncFieldAction.ohoCSWnCKealCohe wWwK© WebhookSvncBatchProcelisteners> MetadataaMicrationP oedriveEh SalesforcealeeldsaOpoortunityVatchenOpportunitySyncStrategyProsoec SaarchStrateoSameatitite@ Client.phpC DecorateActivity.php( DeletcObiectsTrait.phposwan ntone non© PayloadBuilder.phpc) Profile.php© QueryBuilder.php© QueryHandler.phpeQuerviterator.oh© QueryResults.php© Service.php© SyncBatchRedisService.pth TraitsC BaseClient oho( CrmActivitvProviderinteorateCCWCMWMoron©rmobiectctesower.on©.DefaultProsoectSearchStrat.C mallteloer.ond3Findeproscectinterace.onoC) LavouMansoe onoexisiinastaad10.02.23 VasilevDaeosnszol13.10.25 Nikolov10.09.25 iliar240418 Graham24.04.18 Graham190218 Grahan121025 Nikolo20418Grahan19.03.18 Graham20418olahan3.10.252.10.2520.10.21 Graham2.10.25170385 dlian1604.75 Ivanov210 252.04,18Graham3.10.29204,18Graham19.03.18 Graham04418Grahamcrahem2.04.18Graham100218 GrabanC Opportun tvActvitwlatcheeeennortur wewn.Ctestomedrnenont tschd nhrHe OrnenontCostrhSrond nhnWindowServiceTestTO0У L7Inu Lo mey koule[PHONE]0140340440S407408)426428CMatchiet witermints che= IaravellogSF jminny@localhost)© RecordSelector.phg© Activity.phoC) Team.phd# HS local [liminny@localhost# ConbocrhouA console (EU) x iii users (EU)# console (STAGINGTx: AutovSo liminnyBROER PYTnane, M.emare031 A9 A29 V3 /109 A Vclass Service extends BaseService implements01 A7 A149 V1V33 /1 A v 170%public function importStages(?array Stypes = null, ?string SnissingStageNane = nul)): ?Stage1706— 1708SELECT * FRON teans WHERE name LIKE "stounlanes: # 187, 289, 8158SEEiNTUse the Hubspot API client instead of the SDK crmPipelines ) methodencooinr set..certen spineunessndodnro17161711Spipelineskesponse = Sthis->client->getinstance()->getelient()->peguest(method: "BET" , Sen 1712Spipelines = Spipelineskesponse->data->results;carch litecnestSycent sonl Rarkennast Saycentonсллou pexсeaeoth1719foreach (Spipelines as Spipeline) ‹$stages = O— 1715-1728=1720$p = ResponseNornalize::normalizePipeline(Spipeline):SbusinessProcess = Sthis->confiq->businessProcesses@->update0rCreateCd1, t= his-xean->1d.= mb_strinwidth(Spf"label"=> BusinessProcess::TYPEOPPORTUNTTY=> Sol'active').Start 8rwiodt58)=1727=1729=173017391=1732I A recoro tyte re resityp clone of the business process, used to store which recond USe 17s// Create/update record type cloneSthisosconfiaesnecondTvnes(l-sundatehofceatech1736E17571738= 1755etaewdthssolahe"is_selectable=> $p['active'],hetnsce noocnee s Chuesnneepnocneeesdo% oubalD):=1742=1742— 1743-1745aniheCONCAT(U.1d, CASE WHEN U.10 = t.ouner_1d THEN" (owner)" ELSEMrenasisa.*,t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.soclable_idJOIN teans t 1.nc->1: on t.id = u.team_1dWHERE u.tean_id = 187 and sa.provider = 'salesfonce':select * fron activities where id = 31264367select * fron contacts where id = 6331639:select * fron accounts where id = 4156632:select * fron opportunities where id = 4843610:#updateacove#"stage_id' = 132730c0un8-4300-4"contact_id' ="updated at" = 2826-95-22 07:16:select * fron text relavs where created at > '2926-95-91*:select * fron actviates order oy 1d desc;select * fron users where nane loike "subraaSELECT * FROM opportunities HERE unid to_ bin('84a9cfad-2c87-4453-S|select * fron teans where 1d = 557select * fron stages where tean_id = SSS;SETSTMTCONCAT(U.Ld, CASE WHEN U.1d = t.ouner_id THEN" (owner)" ELSElhenasa.*,honnen siil coesasccounteeJOIN users u on u.id = sa.sociable.idahittinnetneosonettsWHERE u.tean_id = 100 and sa.provider = 'hubspot":select id, is_closed, is_won, stage_updated_.at, crm_provider_id, stclose date, forecast category, deleted at, created_at, remotely-crtfron opportunities where tean id = SSS and stageid = 28616 order_Stages - fetch all existing stages upfront to avoid N+1 queriesSexistingStages = $this->config-›stages()->withTrashedO->where("type", Stage::TYPE_ OPPORTUNITY.->getO->kevBy(*crn_providerid')select id. crn-provider id, nane, probability, undated at. deletes=1748Gnon etanodwhere id = 28616foreach (Sof"stages'] as SdealStage)f=Cecsdales Orcnnworeebeeionhmwesotino ooro272464f100 JY-19613 retry on unexpected exception-• git shơw 874C3cea56 - app/Services/Crm/Hubspot/Service-php | grep -A 48 "inportStages"• git show Sfd720fd5b:app/Services/Crm/Hubspot/Service.php | grep -A S8 "function importStages"public function inportStages(farray Stypes u nutl, Zstring SmissingStageNane • null): 75tagonestooee nolt//Use the HubSpot APT client instead of theOokerpinahnado cathoeue ipea e gone inara ponaen a e vratance() -egetCLtent() -request CET:, sendpse);nand git, greo, ta• git show 5td720fd5b:app/Services/Crm/Hubspot/Service.php | grep -A 78 "function importStages" | tail -2:1,'em provider_td' = Ss("1d*"tean_io8, 1915"is_selectableoralabfistyA est'orabahs kew'l 190The probability logic was identical in Nov 2025. Now let me check norealizeDealStage at that timeCommand eit, orec• qit shơw 51d7201d5b:app/Services/Crm/Hubspot/ResponseNormalize.php | grep -A 20 *normalizeDea lStage)public static function normalizebealStage(array|object SdealStage): arraySdata = selt::toArray(SdealStage):displayorder* (ant) (Sdatal"displayOrder") 77 ®)Also loenical. Now exmecoeknelockoealSy toclonest-n00on=n is Wiha ne Now Wo ouKimooturoocho delne oata, Was neledittorant sndnd nt he notkM st thot tmatgtep/ servieisne-bspo esereceices/2o/eusut/S grtee: pPgl xeats -De nes Eropott sh 2a/dev/null | head -solnnthnoXoeCtty lwew ou teoueet today lhay• OKwweunamet4 spad...
|
88392
|
NULL
|
NULL
|
NULL
|
|
88392
|
3013
|
56
|
2026-05-28T17:01:17.609645+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987677609_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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}]...
|
5582423643801155883
|
-8994321436789994556
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
rapstomEV faVsco,ls ~ProjectvCooc#12121 on JY-20963-fix-InHubspotClientinterface.ph© HubspotTokenManager.pt© PayloadBuilder.phpoKimol Cimosewhoo© ResponseNormalize.phpCSeMCPono© SyncFieidAction.phpCSWnCKealCohe wWwK© WebhookSyncBatchProce?—neo aionhelisteners> MetadataMigrationP oedriveSalesforceafeldsOpportunityMatcherOpportunitySyncStrategyProspectSearchStrategy© Cient.php© DecorateActivity.phpG DeleteObjectsTrait.phpoewanarinithooe nha© PayloadBuilder.php© Profile.php©QueryBullder.php©QueryHandler.php©Querylterator.php©QueryResults.php© Service.php© SyncBatchRedisService.ptTraits© BaseCllent.phpCwastemoeene© CachedCrmServiceDecoratorexisiinastaad10.02.23 Vasilev20438Graham24.04.18 Graham24.04.18 Graham3.10.253.10.2513.10.25 Nikolov13.10.25 Nikolov10.09.25 ilian24.04.18 Graham24.04.18 Graham19.03.18 Graham12.10.26 Nikalm2.04.1819.03.18 Graham2.04.18 Graham3.[IP_ADDRESS].10.2520.10.21 Graham17.03.25 ilian2.10.2520438Grsham3.10.25104418Grsham19.03.18 Graham70413GrhamCrmactivity ProviderintegrateCCWCMWMoron20.107 Graham©rmobiectsteso wer.ono© DefaultProspectSearchStrateC Emal telcerond3sindeProscectinternce.onoC) LavouMansoe onoMatchdomsinsysmainteneC Opportun tvActvitwlatche204.187101202.10.2581192.04.18Graham19.03.18 Grahameennortur wewn.CtestomedProspectCache.phpHe OrnenontCostrhSrond nhn4.05.26 Ivanov4.05.26 Ivanovntity // View pull request (today 16:12)WindowU ServiceTest ~Thu 28 May 20:01:17+0.386 Ct387389391393401403[PHONE]28RecordSelector.php© Team,phpE IaravellogA HSJJocal (jiminny@localhost]A console (STAGING]CascadeA console (EU] x E users (EU)les Orcnnworeebeeionhomwwtino ooooThc AutoORDER BY t.name, u.email;• git show 874C3cea56 - app/Services/Cra/Hubspot/Service-php | grep -A 40 "InportStages"class Service extends BaseService inplementspublic function inportStages(?array $types = null, ?string SnissingStageNane = null): ?Stage170601 47 A149 X1 X33 21 A V 1707-17081709SnissingStage = null;17101711try (1712// Use the HubSpot API client instead of the SDK crmPipelines() nethod1713Sendpoint = self::getDealsPipelinesEndpointO):1714SpipeLinesResponse = Sthis-scLient->getInstance() -»getCLient () ->gequest ( method: "GET' , Sen 1715Spipelines = SpipelinesResponse->data-›results;} catch (RequestException|BadRequest Sexception) (-1717ahonspxceor othSe jiminny~031 49 A29 X3 X109 A VE171s=1720foreach (Spipelines as Spipeline) (Sstages = (1:Sp = ResponseNornalize: :normalizePipeline(Spipeline);// Create/update business process for this pipelineSbusinessProcess = Sthis->config->businessProcesses() ->update0rCreate(E=1727-1728-1725=173€asose8eston=> mb_strimwidth(Sp( 'Label'],start: 0,= Business?rocess.:PE OpPoRTumena>> Spl'active'].E1734-1733=17391736Janecond typesis renlly a alone of the business process, lused to stone wich necond1735I/ Create/update record type clone1738Sthinosconfia-snecondTvoes @-sundate0rGeeateChE17391740=1742Sthis->tean->id,nana=> mb_strimwidth(Sp['label'], (start 0, (width: 150),"is„selectable'=> Sp['active'],business_process_id' = SbusinessProcess->id 2? null,—1742=1743=1744=17451746 21748SELECT * FROM teans WHERE nane LIKE "XTourlane%'; # 187, 209, 8154.SELECTCONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE-u.enail,sa.x,t.ouner_id FROM social_accounts saJOIN users u on u.id= sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.tean_idWHERE u.tean_id = 187 and sa.provider = 'salesforce':• qít show 516729105b:ao0/Services/Crm/Hubspot/Service,pho | qreo -A 59 "function iecortStaoes"publie function InportStages(7array stypes = null, Zstring SaissingStageName = null): 75tageSaissingStage = null;Se use the tuosSpipelTnesRes ponge=select * fron activities where id = 31264367:select * fron contacts where id = 6331639;select * fron accounts where id = 4156632;select * fron opportunities where id = 4843610;# updateacove300n8-4300-4"contact_id' = 6.#'stage_id' = 13273,"updated_at' = 2026-05-22 07:16:• git show 51d7281d5b:app/Services/Cmn/Hubspot/Service-php | grep -A 70 "function inportStages" | tail -25craprovloer10 = S$10Batrana raat): 8: 18,select * fron text_relays where created_at > ^2026-05-01';select * fron activities order by id desc;is l'brobabtlity'1 2109)Uhe proosbility looic was idenucal in Nov 2020. now let me check nora lizcoea lStage at that ümeselect * fron users where name like "XSubrak';SELECT * FROM opportunities WHERE wuid_to_bin('04a9cfad-2c87-4453-$select * fron teans where 1d= 555%select * fron stages where tean_id = 555;SETSTMTCONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN ' (owner)' ELSE:lhenasa.*,t.ouner_id FROM social_accounts saJOIN users u on u.id= sa.sociable_idJOIN teans t 1.n<->1: on t.id = u.tean_idWHERE u.tean_id = 100 and sa.provider = "hubspet":• git show 51d728fdSb:app/Services/Crm/Hubspot/ResponseNormalize-php | grep -A 28 "normalizeDealStage"public static funczion nomalizevealStage(array|object sdealstage): array1d' P Sdatal'stageid'!, 27 nult,(int) (Sdata('display0rder'] 27 ®),Also identical. Now let me check the getDealsPipelinesEndpoint - this is what the Nov 2025 bulk import used to fetch pipeline data. Was there aCommand git, xargs, heado git oeserietine -bspor e/Seciees/2/bsp/S giee- ertsP-pl reseropoit shos2>/dev/null | head -sselect id, is_closed, is_won, stage_updated_at, crm_provider_id, stclose_date, forecast_category, deleted_at, created_at, renotely_crtfron opportunities where tean_id = 5SS and stage_id = 28616 order_*select id, crn_provider_id, nane, probability, updated_at, deleterefron stageswhere id = 28616;Run xe (Skip// Stages - fetch all existing stages upfront to avoid N+1 querieses = Sthis->config-›stages()›withTrashedO)->ahere('type', Stage:: TYPE_OPPORTUNITY)Ask anything (XOL)"eoohAdhetKwweuname• O2 4 spac...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
88391
|
3012
|
41
|
2026-05-28T17:01:17.714492+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987677714_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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":"ServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ServiceTest'","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}]...
|
5582423643801155883
|
-8994321436789994556
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:01:178 10Untitled +...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
88390
|
3013
|
55
|
2026-05-28T17:01:12.828366+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987672828_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
1
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'ac...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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":"Show Replace Field","depth":4,"bounds":{"left":0.11868351,"top":0.15482841,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.13131648,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"existingStages","depth":4,"bounds":{"left":0.14228724,"top":0.15403032,"width":0.043882977,"height":0.015961692},"on_screen":true,"value":"existingStages","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.19514628,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.20511968,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.2137633,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.22240691,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/2","depth":4,"bounds":{"left":0.23603724,"top":0.15323225,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.26163563,"top":0.15243416,"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":"Next Occurrence","depth":4,"bounds":{"left":0.27027926,"top":0.15243416,"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":"Filter Search Results","depth":4,"bounds":{"left":0.2789229,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.28756648,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.46210107,"top":0.15243416,"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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.39727393,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.40658244,"top":0.18355946,"width":0.0076462766,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"149","depth":4,"bounds":{"left":0.4162234,"top":0.18355946,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.43018618,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.43949467,"top":0.18355946,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.45179522,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.46077126,"top":0.1819633,"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.4680851,"top":0.1819633,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse HubSpot\\Client\\Crm\\Owners\\Model\\PublicOwner;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->handleOwnersApiException($e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n $profile = $this->processOwner($owner, $teamRepository, $profileRepository);\n\n if ($profile && $userToSearch && $userToSearch->getId() === $profile->getUserId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function handleOwnersApiException(\\HubSpot\\Client\\Crm\\Owners\\ApiException $e): never\n {\n $statusCode = $e->getCode();\n $errorMessage = $e->getMessage();\n $responseBody = $this->parseResponseBody($e->getResponseBody());\n\n $isPermissionError = $this->isPermissionError($statusCode, $errorMessage, $responseBody);\n\n $logContext = [\n 'team_id' => $this->team->getId(),\n 'team_uuid' => $this->team->getUuid(),\n 'config_id' => $this->config->getId(),\n 'status_code' => $statusCode,\n 'error_message' => $errorMessage,\n 'response_body' => $responseBody,\n ];\n\n if ($isPermissionError) {\n $this->logPermissionError($logContext);\n } else {\n $this->logger->error('[HubSpot] Could not sync the profiles.', $logContext);\n }\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n private function parseResponseBody(?string $rawBody): ?array\n {\n if ($rawBody === null || $rawBody === '') {\n return null;\n }\n\n try {\n return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n } catch (\\JsonException) {\n return null;\n }\n }\n\n private function logPermissionError(array $logContext): void\n {\n $this->logger->critical(\n '[HubSpot] ⚠️ PERMISSION ERROR: Cannot sync profiles - Missing OAuth scopes',\n array_merge($logContext, [\n 'action_required' => 'Request additional HubSpot OAuth scopes',\n 'required_scope' => 'crm.objects.owners.read',\n 'impact' => 'Opportunities will have owner_id but NO user_id - AI automation and reporting will fail',\n 'resolution' => 'Manually request scope from HubSpot account admin or re-authenticate',\n ])\n );\n }\n\n private function processOwner(\n PublicOwner $owner,\n TeamRepository $teamRepository,\n ProfileRepository $profileRepository\n ): ?Profile {\n if ($owner->getArchived()) {\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n return null;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n return null;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n if (! $user instanceof User) {\n return null;\n }\n\n return $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n }\n\n private function isPermissionError(int $statusCode, string $errorMessage, ?array $responseBody): bool\n {\n // Check HTTP 403 Forbidden\n if ($statusCode === 403) {\n return true;\n }\n\n // Check for scope-related keywords in error message\n $scopeKeywords = ['scope', 'permission', 'forbidden', 'unauthorized', 'access denied', 'oauth'];\n $lowerErrorMessage = strtolower($errorMessage);\n\n foreach ($scopeKeywords as $keyword) {\n if (str_contains($lowerErrorMessage, $keyword)) {\n return true;\n }\n }\n\n // Check response body for scope errors\n if ($responseBody !== null) {\n return $this->arrayContainsKeyword($responseBody, $scopeKeywords);\n }\n\n return false;\n }\n\n /**\n * Recursively search array for keywords in values\n */\n private function arrayContainsKeyword(array $data, array $keywords): bool\n {\n foreach ($data as $value) {\n if (is_array($value)) {\n if ($this->arrayContainsKeyword($value, $keywords)) {\n return true;\n }\n } elseif (is_string($value)) {\n $lowerValue = strtolower($value);\n foreach ($keywords as $keyword) {\n if (str_contains($lowerValue, $keyword)) {\n return true;\n }\n }\n }\n }\n\n return false;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse HubSpot\\Client\\Crm\\Owners\\Model\\PublicOwner;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->handleOwnersApiException($e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n $profile = $this->processOwner($owner, $teamRepository, $profileRepository);\n\n if ($profile && $userToSearch && $userToSearch->getId() === $profile->getUserId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function handleOwnersApiException(\\HubSpot\\Client\\Crm\\Owners\\ApiException $e): never\n {\n $statusCode = $e->getCode();\n $errorMessage = $e->getMessage();\n $responseBody = $this->parseResponseBody($e->getResponseBody());\n\n $isPermissionError = $this->isPermissionError($statusCode, $errorMessage, $responseBody);\n\n $logContext = [\n 'team_id' => $this->team->getId(),\n 'team_uuid' => $this->team->getUuid(),\n 'config_id' => $this->config->getId(),\n 'status_code' => $statusCode,\n 'error_message' => $errorMessage,\n 'response_body' => $responseBody,\n ];\n\n if ($isPermissionError) {\n $this->logPermissionError($logContext);\n } else {\n $this->logger->error('[HubSpot] Could not sync the profiles.', $logContext);\n }\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n private function parseResponseBody(?string $rawBody): ?array\n {\n if ($rawBody === null || $rawBody === '') {\n return null;\n }\n\n try {\n return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n } catch (\\JsonException) {\n return null;\n }\n }\n\n private function logPermissionError(array $logContext): void\n {\n $this->logger->critical(\n '[HubSpot] ⚠️ PERMISSION ERROR: Cannot sync profiles - Missing OAuth scopes',\n array_merge($logContext, [\n 'action_required' => 'Request additional HubSpot OAuth scopes',\n 'required_scope' => 'crm.objects.owners.read',\n 'impact' => 'Opportunities will have owner_id but NO user_id - AI automation and reporting will fail',\n 'resolution' => 'Manually request scope from HubSpot account admin or re-authenticate',\n ])\n );\n }\n\n private function processOwner(\n PublicOwner $owner,\n TeamRepository $teamRepository,\n ProfileRepository $profileRepository\n ): ?Profile {\n if ($owner->getArchived()) {\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n return null;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n return null;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n if (! $user instanceof User) {\n return null;\n }\n\n return $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n }\n\n private function isPermissionError(int $statusCode, string $errorMessage, ?array $responseBody): bool\n {\n // Check HTTP 403 Forbidden\n if ($statusCode === 403) {\n return true;\n }\n\n // Check for scope-related keywords in error message\n $scopeKeywords = ['scope', 'permission', 'forbidden', 'unauthorized', 'access denied', 'oauth'];\n $lowerErrorMessage = strtolower($errorMessage);\n\n foreach ($scopeKeywords as $keyword) {\n if (str_contains($lowerErrorMessage, $keyword)) {\n return true;\n }\n }\n\n // Check response body for scope errors\n if ($responseBody !== null) {\n return $this->arrayContainsKeyword($responseBody, $scopeKeywords);\n }\n\n return false;\n }\n\n /**\n * Recursively search array for keywords in values\n */\n private function arrayContainsKeyword(array $data, array $keywords): bool\n {\n foreach ($data as $value) {\n if (is_array($value)) {\n if ($this->arrayContainsKeyword($value, $keywords)) {\n return true;\n }\n } elseif (is_string($value)) {\n $lowerValue = strtolower($value);\n foreach ($keywords as $keyword) {\n if (str_contains($lowerValue, $keyword)) {\n return true;\n }\n }\n }\n }\n\n return false;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.47672874,"top":0.123703115,"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.48537233,"top":0.123703115,"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.49634308,"top":0.123703115,"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.5049867,"top":0.123703115,"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.51363033,"top":0.123703115,"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.52460104,"top":0.123703115,"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.5355718,"top":0.123703115,"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.5621675,"top":0.123703115,"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.5731383,"top":0.123703115,"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.64261967,"top":0.123703115,"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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"31","depth":4,"bounds":{"left":0.60039896,"top":0.14844373,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.61203456,"top":0.14844373,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"29","depth":4,"bounds":{"left":0.62200797,"top":0.14844373,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.6343085,"top":0.14844373,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"109","depth":4,"bounds":{"left":0.6442819,"top":0.14844373,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.65791225,"top":0.14684756,"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.66522604,"top":0.14684756,"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":"SELECT * FROM team_features where team_id = 1;\n\nSELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922\nSELECT * FROM users WHERE team_id = 340; # 12015\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 340\nand sa.provider = 'salesforce';\n# and sa.provider = 'salesloft';\n\nselect * from crm_fields where crm_configuration_id = 270 and object_type = 'event';\n# 125558 - Event Type - Event_Type__c\n# 125552 - Event Status - Event_Status__c\n\nSELECT * FROM sidekick_settings WHERE team_id = 340;\n\nSELECT * FROM crm_field_values WHERE crm_field_id in (125552);\n\nselect * from activities where crm_configuration_id = 270\nand type = 'conference' and crm_provider_id IS NOT NULL\nand actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;\n\nSELECT * FROM activities WHERE id = 20871677;\nSELECT * FROM crm_field_data WHERE activity_id = 20871677;\n\nselect * from crm_layouts where crm_configuration_id = 270;\nselect * from crm_layout_entities where crm_layout_id in (886,887);\n\nSELECT * FROM crm_configurations WHERE id = 270;\n\nselect * from playbooks where team_id = 340; # 1514\nselect * from groups where team_id = 340;\nSELECT * FROM crm_fields WHERE id IN (125393, 125401);\n\nselect g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g\njoin playbooks p on g.playbook_id = p.id\njoin crm_fields f on p.activity_field_id = f.id\nwhere g.team_id = 340;\n\nSELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716\nselect * from crm_field_data where object_id = 20448716;\n\nselect * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008\nselect * from opportunities where team_id = 343;\nselect * from opportunities where team_id = 343 and crm_provider_id = '18099102526';\nselect * from opportunities where team_id = 343 and account_id = 945217482;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from accounts where team_id = 343 order by name asc;\n\nselect * from stages where crm_configuration_id = 273 and type = 'opportunity';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143\nSELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;\nSELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';\nSELECT * FROM activities WHERE id = 20717903;\n\nselect * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 353\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, l.atkinson@mwbsolutions.co.uk\nSELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;\n# id: 20940638, user: 12022, contact: 5305871\nSELECT * FROM activity_summary_logs WHERE activity_id = 20940638;\nselect * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 345\nand sa.provider = 'hubspot';\n\nselect * from users where team_id = 345 and id = 12022;\nSELECT * FROM crm_profiles WHERE user_id = 12022;\nSELECT * FROM participants WHERE activity_id = 20940638;\nSELECT * FROM users u\nJOIN crm_profiles cp ON u.id = cp.user_id\nWHERE u.team_id = 345;\n\nselect * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871\n\nselect * from team_features where team_id = 345;\nSELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197\nSELECT * FROM participants WHERE activity_id = 20897406;\n\n\n\nSELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912\nSELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';\n\n\nSELECT * FROM activities WHERE id = 20946641;\nSELECT * FROM crm_profiles WHERE user_id = 10211;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, triger@lunio.ai\nSELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';\nselect * from stages where crm_configuration_id = 97 and type = 'opportunity';\nselect * from opportunities where team_id = 120;\n\n\nselect * from crm_configurations crm join teams t on crm.id = t.crm_id\nwhere 1=1\nAND t.current_billing_plan IS NOT NULL\nAND crm.auto_sync_activity = 0\nand crm.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,james.lewendon@exclaimer.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 270\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956\nSELECT * FROM crm_profiles WHERE user_id = 11446;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, alex.chikly@cygnetise.com\nselect * from playbooks where team_id = 372;\nselect * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340\nSELECT * FROM crm_field_values WHERE crm_field_id = 141340;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 372\nand sa.provider = 'salesforce';\n\nselect * from crm_profiles where crm_configuration_id = 300;\nSELECT * FROM crm_configurations WHERE team_id = 372;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,mfa@planday.com\nSELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756\nselect * from crm_field_data where object_id = 3207756;\nSELECT * FROM crm_fields WHERE id = 111834;\n\nselect f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value\nFROM crm_fields f\nJOIN crm_field_data fd ON f.id = fd.crm_field_id\nWHERE f.crm_configuration_id = 242\nAND f.object_type = 'opportunity'\nAND fd.object_id IN (3207756)\nORDER BY fd.object_id, fd.updated_at;\n\nSELECT * FROM crm_configurations WHERE auto_connect = 1;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,salesforce-admin@tourlane.com\nselect * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id\nwhere g.team_id = 187;\n\nselect * from `groups` where team_id = 187;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 187\nand sa.provider = 'salesforce';\n\n# Destination - 98870 - Destination__c\n# Stage - 79014 - StageName\n# Land Arrangement - 98856 - Land_Arrangement__c\n# Flight - 98848 - Flight__c\n# Last activity date - 98812 - LastActivityDate\n# Last modified date - 98809 - LastModifiedDate\n# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c\n# next call - 98864 - Next_Call__c\n\nselect * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\nselect * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';\nselect * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;\nselect * from activities where opportunity_id = 3538248;\n\nSELECT * FROM crm_profiles WHERE user_id = 8150;\n\nselect * from deal_risks where opportunity_id = 3538248;\n\nselect * from teams where crm_id IS NULL;\n\nSELECT opp.id AS opportunity_id,\n u.group_id AS group_id,\n MAX(\n CASE\n WHEN a.type IN (\"sms-inbound\", \"sms-outbound\") THEN a.created_at\n ELSE a.actual_end_time\n END) as last_date\nFROM opportunities opp\nleft join activities a on a.opportunity_id = opp.id\ninner join users u on opp.user_id = u.id\nwhere opp.user_id IN (9951)\n\nAND opp.is_closed = 0\nand a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL\ngroup by opp.id;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,polly.morphew@cybsafe.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 301;\nSELECT * FROM contacts WHERE id = 6612363;\nSELECT * FROM accounts WHERE id = 4235676;\nSELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;\nselect * from opportunity_stages where opportunity_id = 4503759;\n# SELECT * FROM opportunities WHERE id = 4569937;\n\nselect * from activities where crm_configuration_id = 301;\nSELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370\nSELECT * FROM participants WHERE activity_id = 26330370;\n\nSELECT * FROM teams WHERE id = 375;\nselect * from playbooks where team_id = 375;\n\nselect * from stages where crm_configuration_id = 301 and type = 'opportunity';\n\nselect * from teams;\nselect * from contact_roles;\n\nSELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';\n\nselect * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;\n\nSELECT * FROM crm_field_data WHERE object_id = 3771706;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'\nand crm_provider_id LIKE \"%traffic_light%\";\nSELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);\n\nSELECT fd.* FROM opportunities o\nJOIN crm_field_data fd ON o.id = fd.object_id\nWHERE o.team_id = 343\n# and o.user_id IS NOT NULL\nand fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)\nand fd.value != ''\norder by value desc\n# group by o.id\n;\n\nSELECT * FROM opportunities WHERE id = 3769843;\n\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, salesforce-admin@tourlane.com\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,aswini.mishra@fundingcircle.com\nSELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839\n\n\nSELECT * FROM opportunities WHERE id = 3855992;\n\nSELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988\n\nSELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';\n\nselect * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507\nSELECT * FROM crm_field_data WHERE object_id = 5874411;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379\nand sa.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, nikhil.kumar@mention-me.com\nSELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\nSELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793\nselect * from generic_ai_prompts where subject_id = 3537793;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, triger@lunio.ai\nSELECT * FROM crm_configurations WHERE id = 97;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 97;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;\nSELECT * FROM crm_fields WHERE id = 32682;\n\nselect cfd.value, o.* from opportunities o\njoin crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682\nwhere team_id = 120\nand cfd.value != ''\n;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 120\nand sa.provider = 'salesforce';\n\nselect * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';\nSELECT * FROM crm_field_data WHERE object_id = 2313439;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 410;\nSELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';\nselect * from scorecards where team_id = 410;\nselect * from scorecard_rules;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, aswini.mishra@fundingcircle.com\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\njoin users u on o.user_id = u.id\nwhere a.crm_configuration_id = 177 and a.type LIKE '%email-out%'\n# and a.actual_end_time > '2024-12-16 00:00:00'\n# and o.remotely_created_at > '2024-12-01 00:00:00'\n# and u.group_id = 1014\nand u.id = 9021\norder by a.id desc;\nSELECT * FROM opportunities WHERE id in (3981384,4017346);\nSELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);\n\nselect * from users where id = 9021;\nselect * from inboxes where user_id = 9021;\n\nselect * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';\n\nselect * from email_messages where team_id = 220\nand orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'\nand subject LIKE '%Personal%'\n# and 'from' = 'credit@fundingcircle.com'\n;\n\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\nwhere a.user_id = 9021 and a.type LIKE '%email-out%'\nand a.actual_end_time > '2024-12-18 00:00:00'\nand o.user_id IS NOT NULL\nand o.remotely_created_at > '2024-12-01 00:00:00'\norder by a.id desc;\n\nSELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;\nselect * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;\n\nselect * from team_settings where name IN ('useCloseDate');\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, jfarrell@hurree.co\nSELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 104\nand sa.provider = 'hubspot';\n\nselect * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'\nselect * from teams where crm_id IS NULL;\n\nselect t.name as 'team', u.name as 'owner', u.email, u.phone\nfrom teams t\njoin activity_providers ap on t.id = ap.team_id\njoin users u on t.owner_id = u.id\nwhere 1=1\n and t.status = 'active'\n and ap.is_enabled = 1\n# and u.status = 1\n and ap.provider = 'ms-teams';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nSELECT * FROM teams WHERE id = 442; # 14293\nselect * from users where team_id = 442;\nselect * from social_accounts sa where sa.sociable_id = 14293;\nselect * from invitations where team_id = 442;\n\n# ********************************************************************************************************\nSELECT * FROM users WHERE email LIKE '%nea.liikamaa@eletive.com%'; # 14022\nSELECT * FROM teams WHERE id = 429;\nselect * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);\nselect * from activities where opportunity_id in (4340436,4353519);\n\nselect * from transcription where activity_id IN (25630961,25381771);\nselect * from generic_ai_prompts where subject_id IN (4353519);\n\nSELECT\n a.id as activity_id,\n a.opportunity_id,\n a.type as activity_type,\n a.language,\n CONCAT(a.title, a.description) AS mail_content,\n e.from AS mail_from,\n e.to AS mail_to,\n e.subject AS mail_subject,\n e.body AS mail_body,\n p.type as prompt_type,\n p.status as prompt_status,\n p.content AS prompt_content,\n a.actual_start_time as created_at\nFROM activities a\n LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL\n LEFT JOIN email_messages e ON a.id = e.activity_id\nWHERE a.actual_start_time > '2024-01-01 00:00:00'\n AND a.opportunity_id IN (4353519)\n AND a.status IN ('completed', 'received', 'delivered')\n AND a.deleted_at IS NULL\n AND a.type NOT IN ('sms-inbound', 'sms-outbound')\nORDER BY a.opportunity_id ASC, a.id ASC;\n\nSELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293\nSELECT * FROM teams WHERE id = 442;\nSELECT * FROM crm_configurations WHERE id = 344;\nselect * from team_features where team_id = 442;\nselect * from groups where team_id = 442;\nselect * from playbooks where team_id = 442;\nselect * from playbook_categories where playbook_id = 1729;\nselect * from crm_fields where crm_configuration_id = 344 and id = 172024;\nSELECT * FROM crm_field_values WHERE crm_field_id = 172024;\nselect * from crm_layouts where crm_configuration_id = 344;\nselect * from playbook_layouts where playbook_id = 1729;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444\n\nselect s.*\n# , s.sent_at, u.name, a.*\nfrom activity_summary_logs s\ninner join activities a on a.id = s.activity_id\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 356\nand s.sent_at > date_sub(now(), interval 60 day)\norder by a.actual_end_time desc;\n\nselect * from activities a\n# inner join activity_summary_logs s on s.activity_id = a.id\nwhere a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)\n# and a.crm_provider_id is not null\n# and provider <> 'ringcentral'\nand status = 'completed'\norder by a.actual_end_time desc;\n\nselect * from teams order by id desc; # 17328, 32, 17830, integration-account@jiminny.com\nSELECT * FROM users;\nSELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active\nSELECT * FROM teams WHERE id = 260;\nselect * from team_settings where team_id = 260;\nselect * from crm_configurations where team_id = 260;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 356;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;\n\nselect * from accounts where crm_configuration_id = 221 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 221 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 221 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 221 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 221;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 221 order by id desc;\nselect * from stages where crm_configuration_id = 221 order by id desc;\n\nselect * from accounts where crm_configuration_id = 356 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 356 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 356 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 356 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 356;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 356 order by id desc;\nselect * from stages where crm_configuration_id = 356 order by id desc;\n\nselect * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)\nselect * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)\nselect * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4\nselect ce.* from calendars c\njoin users u on c.user_id = u.id\njoin calendar_events ce on c.id = ce.calendar_id\nwhere u.team_id = 260\nand (ce.start_time > '2025-02-21 00:00:00')\n;\n# calendar events 1207\n#\n\nselect * from opportunities where team_id = 260;\nSELECT * FROM crm_field_data WHERE object_id = 4696496;\n\nselect * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;\nselect * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')\n# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0\nand created_at > '2024-03-01 00:00:00'\norder by id desc; # 880 000, ringcentral, avaya\nSELECT * FROM participants WHERE activity_id = 26371744;\n\n# all activities 942 000 +\n# conference 7385 - scheduled 984 - external 343\n\nselect * from activities where id = 26321812;\nselect * from participants where activity_id = 26321812;\nselect * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);\nselect * from leads where id in (720428,689175,731546,645866,621037);\n\nselect * from users where id = 13841;\nselect * from opportunities where user_id = 9541;\nselect * from stages where id = 15900;\n\nselect * from accounts where\n# id IN (4160055,5053725,4965303,4896434)\nid in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)\n;\n\nselect * from activities where id = 26654935;\nSELECT * FROM opportunities WHERE id = 4803458;\n\nSELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;\nSELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time\nFROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);\n\nSELECT DISTINCT\n o.id, o.stage_id, s.name, a.title,\n a.*\nFROM activities a\n# INNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nINNER JOIN groups g ON u.group_id = g.id\nINNER JOIN opportunities o ON a.opportunity_id = o.id\nINNER JOIN stages s ON o.stage_id = s.id\nWHERE\n a.crm_configuration_id = 356\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 13841\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')\n AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')\n\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n )\n )\n AND (\n# s.id = 15900\n s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')\n OR s.uuid IS NULL -- Include records without opportunity stage\n )\n\nORDER BY a.actual_end_time DESC;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, willsc@leadforensics.com\nSELECT * FROM users WHERE team_id = 190;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 190\nand sa.provider = 'hubspot';\n\nselect * from role_user where user_id = 8474;\n\nselect * from crm_configurations where provider = 'bullhorn';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;\nSELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;\n\nSELECT * FROM opportunities WHERE id = 4732493;\nselect * from activities where opportunity_id = 4732493;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 443; # 358, 14315, andrea.romano@correrenaturale.com\nSELECT * FROM opportunities WHERE team_id = 443;\n\nSELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id\nFROM activities AS a\nJOIN stages AS s ON a.stage_id = s.id\nJOIN users AS u ON u.id = a.user_id\nJOIN teams AS t ON t.id = s.team_id\nWHERE u.team_id <> s.team_id and t.id > 135;\n\n\nSELECT\n crm_configuration_id,\n crm_provider_id,\n COUNT(*) as duplicate_count,\n GROUP_CONCAT(id) as stage_ids,\n GROUP_CONCAT(name) as stage_names\nFROM stages\nGROUP BY crm_configuration_id, crm_provider_id\nHAVING COUNT(*) > 1\nORDER BY duplicate_count DESC;\n\nselect * from stages where id IN (14898,14907);\n\nselect * from business_processes;\n\nSELECT *\nFROM crm_configurations\nWHERE team_id IN (\n SELECT team_id\n FROM crm_configurations\n GROUP BY team_id\n HAVING COUNT(*) > 1\n)\nORDER BY team_id;\n\nSELECT *\nFROM teams\nWHERE crm_id IN (\n SELECT crm_id\n FROM teams\n GROUP BY crm_id\n HAVING COUNT(*) > 1\n)\nORDER BY crm_id;\n\n# ***************************************************************************\nselect * from crm_configurations where provider = 'integration-app';\nSELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 andrea.romano@correrenaturale.com\nselect * from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect * from team_features where team_id = 358;\nselect * from activity_summary_logs;\n\nselect * from teams where id = 406;\n\n# ************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, srv.salesforce@sportfive.com\nselect * from activities where crm_configuration_id = 202 order by actual_end_time desc;\n\nSELECT * FROM users where id = 14637;\nSELECT * FROM teams where id = 267;\nSELECT * FROM groups where id = 1118;\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 202\n AND status IN ('completed', 'failed')\n AND recording_state != 'stopped'\n AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n AND (is_private = 0 OR user_id = 14637)\n AND (\n (\n actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n ) OR (\n actual_start_time IS NULL\n AND type IN ('sms-outbound', 'sms-inbound')\n AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tracks\n WHERE\n tracks.activity_id = activities.id\n AND tracks.type IN ('audio', 'video')\n )\nORDER BY actual_end_time DESC;\n\nSELECT DISTINCT\n a.*\nFROM activities a\nINNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nWHERE\n a.crm_configuration_id = 202\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 14637\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND a.user_id = 14637\n )\n )\n\nORDER BY a.actual_end_time DESC\n;\n\nSELECT DISTINCT a.*\nFROM activities a\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams t ON u.team_id = t.id\n# INNER JOIN tracks tr ON a.id = tr.activity_id\n# INNER JOIN groups g ON u.group_id = g.id\nWHERE 1=1\n AND t.id = 267\n# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND tr.type NOT IN ('audio', 'video')\n AND (\n a.is_private = 0\n OR a.user_id = 14637\n )\n AND (\n (a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')\n OR (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'\n )\n )\n# and NOT EXISTS (\n# SELECT 1\n# FROM tracks t\n# WHERE t.activity_id = a.id\n# AND t.type IN ('audio', 'video')\n# )\n\nORDER BY a.actual_end_time DESC;\n\nSELECT * FROM tracks WHERE activity_id = 26485995;\n\nselect a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 202\n# and a.is_internal = 0\nand (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type IN (\"softphone\",\"softphone-inbound\",\"conference\",\"sms-inbound\")\nand a.status IN ('completed', 'failed')\n# and a.external_id is not null\norder by a.actual_end_time desc;\n\nselect * from activities a where a.crm_configuration_id = 202\nand a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'\n# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_field_data WHERE crm_field_id = 98809;\n\nselect * from users where status = 1 AND timezone = 'MDT';\n\nselect * from opportunities where id = 3769814;\nselect * from deal_risks where opportunity_id = 3769814;\n\nselect cp.* from crm_profiles cp\njoin users u on cp.user_id = u.id\njoin crm_configurations crm on cp.crm_configuration_id = crm.id\nwhere crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';\n\nselect * from crm_fields where id = 154575;\n\nselect * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';\nSELECT * FROM teams WHERE id = 176; # crm 148\nselect * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nselect * from crm_fields cf\njoin crm_configurations crm on crm.id = cf.crm_configuration_id\nwhere crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');\n\n# *********************************************************************************************\nSELECT * FROM users WHERE id IN (15415, 15418);\nSELECT * FROM groups WHERE id IN (1805,1806);\nSELECT * FROM playbooks WHERE id = 1860;\nSELECT * FROM playbook_categories WHERE id = 38634;\nSELECT * FROM crm_fields WHERE id = 189962;\n\nSELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 raza.gilani@vuelio.com\n\nSELECT * FROM crm_profiles WHERE user_id = 15415;\nSELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';\n\nselect * from sidekick_settings where team_id = 472;\n\nSELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418\nSELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415\n\n# *********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, salesforce-integrations@teamtailor.com\nselect * from crm_configurations where id = 218;\nSELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765\nSELECT * FROM users WHERE id IN (13232, 13230);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n0057R00000EPL5HQAX Inez Ekblad\n\n1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur\n\nSELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);\n\n############################################################################################\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id IN (94491,94493,94498);\nSELECT * FROM users WHERE id = 13658;\nSELECT * FROM teams WHERE id = 109;\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, katy.holden@strengthscope.comk\nSELECT * FROM stages WHERE crm_configuration_id = 390;\nselect * from business_processes where team_id = 481 and crm_configuration_id = 390;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 481\nand sa.provider = 'salesforce';\n\n\nSELECT * FROM users WHERE id = 15780; # team 462\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 462\nand sa.provider = 'hubspot';\n\n\nselect * from teams where id = 495;\nSELECT * FROM users WHERE id = 15794;\nselect * from social_accounts where sociable_id = 15794;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752\nSELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794\nSELECT * FROM activities WHERE crm_configuration_id = 407\nand status = 'completed' and type = 'conference'\norder by id desc;\n\nselect ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id\njoin permission_role pr on pr.role_id = ru.role_id\n join permissions p on p.id = pr.permission_id\nwhere team_id = 495 and p.name IN ('dial');\n\nselect * from permission_role;\n\nselect * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;\nSELECT * FROM activities WHERE id = 29512773;\nSELECT * FROM activities WHERE id IN (29042721,28991325,29002874);\n\nSELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 407\n# and a.id IN (29042721,28991325,29002874);\n\nSELECT * FROM users WHERE id = 15794;\nSELECT * FROM users WHERE team_id = 495;\nSELECT * FROM social_accounts WHERE sociable_id = 15794;\nSELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';\nSELECT * FROM contacts WHERE team_id = 495;\nSELECT * FROM leads WHERE team_id = 495;\nSELECT * FROM accounts WHERE team_id = 495;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 407;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 407;\nSELECT * FROM crm_configurations WHERE id = 407;\nSELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'\nand user_id IS NOT NULL and is_closed = 1 and is_won = 1;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103\nSELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064\nSELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');\n\n# *********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 325\nand sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085\nSELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733\nSELECT * FROM activity_summary_logs where activity_id = 28719733;\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444\nSELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';\nSELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630\nselect * from activities where crm_configuration_id = 356 and lead_id = 841732;\n\nSELECT * from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 356;\n\nselect * from activities where crm_configuration_id = 356\nand actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'\norder by id desc;\n\nselect * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;\nselect * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\n\nselect * from team_features where team_id = 260;\nselect * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;\n\nselect * from crm_fields;\nselect * from crm_layout_entities;\n\nSELECT * FROM teams WHERE name LIKE '%Optable%';\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id in (94491,94493,94498);\n\nselect * from teams where crm_id IS NULL;\n\nSELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;\n\n# *************************************************************************************************\nselect * from team_domains where team_id = 399;\nSELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207\n\nselect * from calendar_events where id = 5163781;\nSELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896\nSELECT * FROM participants WHERE activity_id = 29443896;\nselect * from contacts where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\nselect * from leads where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\n\nselect * from activities where user_id = 14937 order by created_at ;\n\nselect * from users where id = 14937;\n\nselect * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';\nselect * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';\n\nselect * from activities a join participants p on a.id = p.activity_id\nwhere crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';\n\n# *************************************************************************************************\nSELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';\nSELECT * FROM opportunities WHERE team_id = 379 order by id desc;\nSELECT * FROM teams WHERE id = 379;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379 and sociable_id = 13852\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE id = 307;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 307;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307\n and id IN (144750,144855,145158,155227);\n\nSELECT * FROM activities;\n\n\nselect * from activities\nwhere created_at > '2025-07-01 00:00:00'\n# and created_at < '2025-08-01 00:00:00'\nand type not in ('email-outbound', 'email-inbound')\nand account_id is null\nand contact_id is null\nand lead_id is null\nand opportunity_id is not null\n;\nSELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);\nSELECT * FROM crm_configurations WHERE id in (335,301,200);\n\nselect * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';\n\nSELECT * FROM teams WHERE name LIKE '%Resights%';\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nselect * from teams where id IN (442);\n\nselect * from activities\nwhere crm_configuration_id = 177\nand provider = 'amazon-connect'\n order by id desc;\n# and source <> 'gong';\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nSELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;\n\n\nselect * from crm_configurations where store_transcript = 1;\nSELECT * FROM teams WHERE id IN (80);\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 277\nand sa.provider = 'salesforce';\n\nselect * from activities where crm_configuration_id = 213 and account_id = 2511502;\n\nselect * from crm_configurations where id = 213;\n\nSELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604\nSELECT * FROM participants WHERE activity_id = 33981604;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 431\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223\nselect * from activity_summary_logs where activity_id = 33997223;\nselect * from activity_notes where activity_id = 33997223;\n\n# ***********************************\nSELECT * FROM teams WHERE name LIKE '%Abode%';\n\n\nselect * from features;\nselect * from teams t\nwhere t.status = 'active'\nand id NOT IN (select team_id from team_features where feature_id = 9)\n;\n\n\nselect * from playbook_layouts where playbook_id = 1725;\nSELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473\nselect * from teams where id = 318;\nselect * from crm_configurations where team_id = 318;\nselect * from playbooks where team_id = 318;\nSELECT * FROM crm_layouts where crm_configuration_id = 381;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;\nSELECT * FROM crm_fields WHERE id IN (192938,192936,192939);\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;\nSELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);\n\nSELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533\n\n\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nSELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);\n\nSELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124\nselect * from crm_layouts where crm_configuration_id = 281;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nselect * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);\nselect * from opportunities where crm_configuration_id = 281;\n\nSELECT * FROM activities WHERE id IN (34211315, 34130075);\nSELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);\n\nselect cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd\njoin crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id\njoin crm_fields cf on cle.crm_field_id = cf.id\nwhere cf.deleted_at IS NOT NULL\nGROUP BY cle.id, cf.id;\n\nselect * from crm_layouts where id IN (355);\nselect u.email, t.crm_id, t.* from teams t\njoin users u on u.id = t.owner_id\nwhere crm_id IN (97);\n\nSELECT * FROM crm_fields WHERE id = 96492;\n\nselect * from permissions;\nselect * from permission_role where permission_id = 247;\nselect * from roles;\n\nselect * from migrations;\n# *****************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('291e3c21-11cc-4728-aee7-6e4bedf86d72') = uuid; # 34262174\nSELECT * FROM crm_configurations WHERE id = 301;\nSELECT * FROM teams WHERE id = 343;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from participants where activity_id = 34262174;\n\nselect * from contacts where crm_configuration_id = 301 and id = 6976326;\nselect * from accounts where crm_configuration_id = 301 and id IN (4647626, 4815829); # 30761335403\n\nselect * from activity_summary_logs where activity_id = 34262174;\n\nselect * from users where status = 1 AND timezone = 'EST';\n\n# ****************************************************************************\nSELECT * FROM users WHERE id = 13869;\nSELECT * FROM crm_configurations WHERE id = 320;\nSELECT * FROM teams WHERE id = 401;\n\nSELECT * FROM activities WHERE uuid_to_bin('2228c16f-10be-48d5-90d4-67385219dc01') = uuid; # 29670601\n\nSELECT * FROM accounts WHERE id = 7761483;\nSELECT * FROM opportunities WHERE id = 6051814;\n\nSELECT * FROM teams WHERE name LIKE '%Seedlegals%';\n\n;select * from opportunities where updated_at > '2025-10-11' AND crm_provider_id = '34713761166';\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 177;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 577;\nSELECT * FROM crm_fields WHERE id IN (68458,68459,68480,68497,68524,68530,68554,68618,68662,68781,68810,68898,68981,69049,97467);\n\nSELECT t.id, crm.id, t.name, crm.sync_objects, crm.provider, crm.last_synced_at FROM crm_configurations crm join teams t on t.crm_id = crm.id\nwhere t.status = 'active' AND crm.provider = 'hubspot' AND crm.last_synced_at < '2025-10-22 00:00:00';\n\nSELECT * FROM activities WHERE uuid_to_bin('fa09449f-cba9-496a-b8f3-865cd3c72351') = uuid;\nSELECT * FROM crm_configurations where id = 184;\nSELECT * FROM teams WHERE id = 246;\nSELECT * FROM social_accounts WHERE sociable_id = 9259 and provider = 'hubspot';\n\nSELECT * FROM users WHERE email LIKE '%rhian.old@bud.co.uk%'; # 17700\nSELECT * FROM teams WHERE id = 551;\n\nSELECT * FROM crm_configurations WHERE id = 471;\nSELECT * FROM activities WHERE crm_configuration_id = 471 and crm_provider_id IS NOT NULL;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 471;\nSELECT * FROM crm_fields WHERE id = 307260;\nSELECT * FROM crm_field_values WHERE crm_field_id = 307260;\n\nselect * from crm_layouts where crm_configuration_id = 471;\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1547;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1548;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 551 and sa.provider = 'hubspot';\n\nSELECT * FROM teams WHERE name LIKE '%$PCS%';\n\n# ********************************************************************************************************\nselect * from crm_configurations crm\njoin teams t on t.crm_id = crm.id\nwhere t.status = 'active'\nand crm.provider = 'hubspot';\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 crm_configurations where id = 331; # 416\nSELECT * FROM teams WHERE id = 416;\nSELECT * FROM opportunities WHERE team_id = 190;\n\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%';\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 190 and sa.provider = 'hubspot';\n\n\n\nSELECT * FROM teams WHERE name LIKE '%Rapaport%'; # 431, 337\nSELECT * FROM teams where id = 431;\nSELECT * FROM crm_configurations where team_id = 431;\nSELECT * FROM activity_providers where team_id = 431;\nSELECT * FROM activities where crm_configuration_id = 337 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 431 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%BiP%'; # 401, 320\nSELECT * FROM teams where id = 401;\nSELECT * FROM crm_configurations where team_id = 401;\nSELECT * FROM activity_providers where team_id = 401;\nSELECT * FROM activities where crm_configuration_id = 320 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 401 and sa.provider = 'salesforce';\n\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 307; # 379 - Story Terrace Inc , portalId: 3921157\nSELECT * FROM contacts WHERE team_id = 379 and updated_at > '2026-01-31 11:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 379 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; # 563 - LATUS Group (ad94d501-5d09-44fd-878f-ca3a9f8865c3) , portalId: 3904501\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 338; # 432 - Formalize , portalId: 9214205\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 432 and sa.provider = 'hubspot';\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 436; # 519 - Moxso , portalId: 25531989\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 96; # 119 - Nourish Care , portalId: 26617984\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 331; # 416 - The National College , portalId: 7213852\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 308; # 380 - Foodles , portalId: 7723616\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 379; # 471 - imat-uve , portalId: 9177354\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 465; # 545 - Spotler , portalId: 144759271\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 455; # 537 - indevis , portalId: 25666868\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 200; # 265 - Jobadder , portalId: 6426676\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 335; # 429 - Eletive , portalId: 6110563\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 363; # 456 - Global Group , portalId: 8901981\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 297; # 369 - Unbiased , portalId: 9229005\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 353; # 449 - Fuuse , portalId: 25781745\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 487; # 566 - Nimbus , portalId: 39982590\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 487;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1630;\nselect * from crm_fields where crm_configuration_id = 487 and\n(uuid_to_bin('4c6b2971-64d4-45b8-b377-427be758b5a5') = uuid or uuid_to_bin('59e368d8-65a0-4b77-b611-db37c99fbe68') = uuid);\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 420; # 506 - voiio , portalId: 145629154\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 479; # 558 - Momice , portalId: 535962\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 59; # 80 - Storyclash GmbH , portalId: 4268479\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 175; # 203 - Team iAM , portalId: 5534732\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 368; # 460 - OneTouch Health , portalId: 5534732183355\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\n\n\nselect * from users where id = 29643;\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM teams WHERE name LIKE '%Buynomics%'; # 462, 482, 14910\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\n# and description like '%The call focused on understanding Welch%'\norder by id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 462 and sa.provider = 'salesforce';\n\nselect * from contacts where crm_configuration_id = 482 and name = 'Cyndall Hill'; # 15504749\nselect * from contacts where id = 10891096; # 482\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\nand contact_id = 15504749\norder by id desc;\n\nselect * from activities where id = 36793003; # 96cc7bc1-8622-4d27-92f4-baf664fc1a56, 00UOf00000PDdOXMA1\nselect * from transcription where id = 7646782;\nselect * from ai_prompts where transcription_id = 7646782;\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7a8471a3-847e-4822-802b-ddf426bbc252') = uuid; # 37370018\nSELECT * FROM activity_summary_logs WHERE activity_id = 37370018;\nSELECT * FROM teams WHERE id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 555 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7c17b8aa-09df-4f85-a0f7-51f47afd712d') = uuid; # 37395250\nSELECT * FROM activities WHERE uuid_to_bin('14d60388-260d-494b-aa0d-63fdb1c78026') = uuid; # 37395250\n\nSELECT a.* FROM activities a JOIN crm_configurations c on c.id = a.crm_configuration_id\nwhere a.type IN ('softphone', 'softphone-outbound') and c.provider = 'hubspot'\nand a.provider NOT IN ('hubspot')\n# and a.provider IN ('salesloft')\n# and c.id NOT IN (70)\n# and a.duration > 30\n# and actual_start_time > '2026-02-05 00:00:00'\norder by a.id desc;\n\nSELECT * FROM activities WHERE id = 37549787;\nSELECT * FROM crm_profiles WHERE user_id = 17613;\n\nSELECT * FROM crm_configurations WHERE id = 70;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 93 and sa.provider = 'hubspot';\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations WHERE id = 373; # KPSBremen.de 465 # - no social account\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 465 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 494;\n\nSELECT * FROM teams WHERE name LIKE '%splose%'; # 572, 495, 18708\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 572 and sa.provider = 'pipedrive';\n\nselect * from opportunities where team_id = 572\n# and name like '%Onebright%'\n# and is_closed = 1 and is_won = 0\n order by id desc;\n\n\nselect * from users where deleted_at is null and status = 2;\n\nselect * from contacts where id = 17900517;\nselect * from accounts where id = 10109838;\nselect * from opportunities where id = 6955880;\n\nselect * from opportunity_contacts where opportunity_id = 6955880;\nselect * from opportunity_contacts where contact_id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nSELECT * FROM activities WHERE uuid_to_bin('adcb8331-5988-4353-834e-383a355abba2') = uuid; # 38056424, crm 104659682404\nselect * from teams where id = 456;\nSELECT * FROM crm_configurations WHERE id = 363;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 456 and sa.provider = 'hubspot';\n\nselect * from crm_layouts where crm_configuration_id = 363;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (1203, 1204, 1635);\nSELECT * FROM crm_fields WHERE id IN (181536, 181538, 213455);\n\nSELECT * FROM teams WHERE name LIKE '%Electric%'; # 342, 272, 12767\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and name like 'NORTHUMBRIA POL%'; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 order by remotely_created_at asc; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and updated_at > '2026-01-01 00:00:00';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 272 and object_type = 'opportunity';\nSELECT * FROM crm_field_values WHERE crm_field_id = 127164;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\n\nSELECT * FROM teams WHERE id = 472;\nSELECT * FROM crm_configurations WHERE id = 380;\nselect * from activities where id = 38285673; # 38285673\nSELECT * FROM users WHERE id = 16942;\nSELECT * FROM groups WHERE id = 1964;\nSELECT * FROM playbooks WHERE id = 2033;\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 499; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1678;\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\n\nSELECT * FROM activities WHERE uuid_to_bin('96b1261f-2357-49f9-ab38-23ce12008ea0') = uuid;\n\nselect * from contacts c\nwhere c.crm_configuration_id = 370 order by c.updated_at desc;\n\nSELECT * FROM participants where activity_id = 38833541;\nSELECT * FROM participants where activity_id = 39216301;\nSELECT * FROM activity_summary_logs where activity_id = 39216301;\nSELECT * FROM activities WHERE uuid_to_bin('c7d99fbe-1fb1-41f2-8f4d-52e2bf70e1e9') = uuid; # 38833541, crm 478116564181\nSELECT * FROM activities WHERE uuid_to_bin('2e6ff4d3-9faa-447a-a8c1-9acde4d885ae') = uuid; # 39216301, crm 480171536586\nselect * from crm_profiles where crm_configuration_id = 319 and crm_provider_id = 525785080;\nselect * from opportunities where crm_configuration_id = 319 and crm_provider_id = 410150124747;\nselect * from accounts where crm_configuration_id = 319 and crm_provider_id = 47150650569;\nselect * from contacts where crm_configuration_id = 319 and crm_provider_id IN ('665587441856', '742723347700');\n# owner 13236 525785080\n# contact 1 16779180 665587441856 - activity - Alex Howes alex@supportroom.com created 2026-01-26\n# contact 2 19247563 742723347700 - ash@supportroom.com 2026-03-24\n# company 4176133 47150650569\n# deal 7100953 410150124747\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 400 and sa.provider = 'hubspot';\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556; # owner: 18101, crm: 477\nselect * from crm_configurations where id = 477;\nSELECT * FROM users WHERE id = 18101;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'integration-app';\n\nselect * from opportunities where id = 7594349;\nselect * from opportunity_stages where opportunity_id = 7594349 order by created_at desc;\nselect * from business_processes where id = 6024;\nselect * from business_process_stages where stage_id = 16352;\nselect * from business_process_stages where business_process_id = 6024;\nselect * from stages where team_id = 459;\nselect * from teams where id = 459;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 459 and sa.provider = 'hubspot';\n\nSELECT os.stage_id, s.crm_provider_id, s.name, COUNT(*) as cnt\nFROM opportunity_stages os\nJOIN stages s ON s.id = os.stage_id\nWHERE os.opportunity_id = 7594349\nGROUP BY os.stage_id, s.crm_provider_id, s.name\nORDER BY cnt DESC;\n\nSELECT s.id, s.crm_provider_id, s.name, s.team_id, s.crm_configuration_id\nFROM stages s\nJOIN business_process_stages bps ON bps.stage_id = s.id\nWHERE bps.business_process_id = 6024\nAND s.crm_provider_id = 'contractsent';\n\nselect * from stages where id IN (16352,20612,18281,7344,16378,16309,5036,15223,14535,6293,12098,11607)\n\nSELECT * FROM teams WHERE name LIKE '%Pulsar Group%'; # 472, 380, 15138, raza.gilani@vuelio.com\nselect * from playbooks where team_id = 472; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 2288;\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 380;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 472 and sa.provider = 'salesforce';\n\nselect * from activities where id = 58081273;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2;\n\nSELECT * FROM users WHERE name LIKE '%Neil Hoyle%'; # 17651\nSELECT * FROM social_accounts WHERE sociable_id = 17651;\n\nSELECT * FROM activities WHERE uuid_to_bin('975c6830-7d49-4c1e-b2e9-ac80c10a738a') = uuid;\nSELECT * FROM opportunities WHERE id IN (7842553, 6211727);\nSELECT * FROM contacts WHERE id IN (10202724, 6211727);\nSELECT * FROM opportunity_stages WHERE opportunity_id = 7842553;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 519 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 436;\nselect * from crm_profiles where crm_configuration_id = 436; # 76091797 -> 16612\n\nselect * from contact_roles where contact_id = 10202724;\n\nselect * from stages where team_id = 519; # 18778\n18775\n\nSELECT\n id,\n crm_provider_id,\n stage_id,\n is_closed,\n is_won,\n stage_updated_at,\n updated_at\nFROM opportunities\nWHERE id IN (6211727, 7842553);\n\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id = 6211727 AND contact_id = 10202724;\n\nSELECT id, name, stage_id, is_closed, is_won, updated_at, remotely_created_at\nFROM opportunities\nWHERE account_id = 8179134\nORDER BY updated_at DESC;\n\n\nselect * from text_relays where created_at > '2026-01-01';\nAND id IN (691, 692);\n\nselect * from teams;\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = a.user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nSELECT DISTINCT u.id, u.email, u.name, u.team_id, t.name as team_name,\n t.twilio_sms_sid, t.twilio_messaging_sid\nFROM users u\nINNER JOIN teams t ON u.team_id = t.id\nWHERE (t.twilio_sms_sid IS NOT NULL OR t.twilio_messaging_sid IS NOT NULL)\nAND u.status = 1\nORDER BY t.name, u.email;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 187 and sa.provider = 'salesforce';\n\nselect * from activities where id = 31264367;\nselect * from contacts where id = 6331639;\nselect * from accounts where id = 4156632;\nselect * from opportunities where id = 4843610;\n# update `activities` set `account_id` = 4156632, `contact_id` = 6331639, `opportunity_id` = 4843610,\n# `stage_id` = 13273, `activities`.`updated_at` = 2026-05-22 07:16:17 where `id` = 31264367)\"\n\nselect * from text_relays where created_at > '2026-05-01';\n\nselect * from activities order by id desc;\n\nselect * from users where name like '%Subra%';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('04a9cfad-2c87-4453-9e72-20aeb78ccf8d') = uuid;\nselect * from teams where id = 555;\nselect * from stages where team_id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 100 and sa.provider = 'hubspot';\n\nselect id, is_closed, is_won, stage_updated_at, crm_provider_id, stage_id, probability,\nclose_date, forecast_category, deleted_at, created_at, remotely_created_at, updated_at\nfrom opportunities where team_id = 555 and stage_id = 20616 order by updated_at desc limit 10;\n\nselect id, crm_provider_id, name, probability, updated_at, deleted_at\nfrom stages\nwhere id = 20616;","depth":4,"on_screen":true,"value":"SELECT * FROM team_features where team_id = 1;\n\nSELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922\nSELECT * FROM users WHERE team_id = 340; # 12015\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 340\nand sa.provider = 'salesforce';\n# and sa.provider = 'salesloft';\n\nselect * from crm_fields where crm_configuration_id = 270 and object_type = 'event';\n# 125558 - Event Type - Event_Type__c\n# 125552 - Event Status - Event_Status__c\n\nSELECT * FROM sidekick_settings WHERE team_id = 340;\n\nSELECT * FROM crm_field_values WHERE crm_field_id in (125552);\n\nselect * from activities where crm_configuration_id = 270\nand type = 'conference' and crm_provider_id IS NOT NULL\nand actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;\n\nSELECT * FROM activities WHERE id = 20871677;\nSELECT * FROM crm_field_data WHERE activity_id = 20871677;\n\nselect * from crm_layouts where crm_configuration_id = 270;\nselect * from crm_layout_entities where crm_layout_id in (886,887);\n\nSELECT * FROM crm_configurations WHERE id = 270;\n\nselect * from playbooks where team_id = 340; # 1514\nselect * from groups where team_id = 340;\nSELECT * FROM crm_fields WHERE id IN (125393, 125401);\n\nselect g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g\njoin playbooks p on g.playbook_id = p.id\njoin crm_fields f on p.activity_field_id = f.id\nwhere g.team_id = 340;\n\nSELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716\nselect * from crm_field_data where object_id = 20448716;\n\nselect * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008\nselect * from opportunities where team_id = 343;\nselect * from opportunities where team_id = 343 and crm_provider_id = '18099102526';\nselect * from opportunities where team_id = 343 and account_id = 945217482;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from accounts where team_id = 343 order by name asc;\n\nselect * from stages where crm_configuration_id = 273 and type = 'opportunity';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143\nSELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;\nSELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';\nSELECT * FROM activities WHERE id = 20717903;\n\nselect * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 353\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, l.atkinson@mwbsolutions.co.uk\nSELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;\n# id: 20940638, user: 12022, contact: 5305871\nSELECT * FROM activity_summary_logs WHERE activity_id = 20940638;\nselect * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 345\nand sa.provider = 'hubspot';\n\nselect * from users where team_id = 345 and id = 12022;\nSELECT * FROM crm_profiles WHERE user_id = 12022;\nSELECT * FROM participants WHERE activity_id = 20940638;\nSELECT * FROM users u\nJOIN crm_profiles cp ON u.id = cp.user_id\nWHERE u.team_id = 345;\n\nselect * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871\n\nselect * from team_features where team_id = 345;\nSELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197\nSELECT * FROM participants WHERE activity_id = 20897406;\n\n\n\nSELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912\nSELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';\n\n\nSELECT * FROM activities WHERE id = 20946641;\nSELECT * FROM crm_profiles WHERE user_id = 10211;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, triger@lunio.ai\nSELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';\nselect * from stages where crm_configuration_id = 97 and type = 'opportunity';\nselect * from opportunities where team_id = 120;\n\n\nselect * from crm_configurations crm join teams t on crm.id = t.crm_id\nwhere 1=1\nAND t.current_billing_plan IS NOT NULL\nAND crm.auto_sync_activity = 0\nand crm.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,james.lewendon@exclaimer.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 270\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956\nSELECT * FROM crm_profiles WHERE user_id = 11446;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, alex.chikly@cygnetise.com\nselect * from playbooks where team_id = 372;\nselect * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340\nSELECT * FROM crm_field_values WHERE crm_field_id = 141340;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 372\nand sa.provider = 'salesforce';\n\nselect * from crm_profiles where crm_configuration_id = 300;\nSELECT * FROM crm_configurations WHERE team_id = 372;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,mfa@planday.com\nSELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756\nselect * from crm_field_data where object_id = 3207756;\nSELECT * FROM crm_fields WHERE id = 111834;\n\nselect f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value\nFROM crm_fields f\nJOIN crm_field_data fd ON f.id = fd.crm_field_id\nWHERE f.crm_configuration_id = 242\nAND f.object_type = 'opportunity'\nAND fd.object_id IN (3207756)\nORDER BY fd.object_id, fd.updated_at;\n\nSELECT * FROM crm_configurations WHERE auto_connect = 1;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,salesforce-admin@tourlane.com\nselect * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id\nwhere g.team_id = 187;\n\nselect * from `groups` where team_id = 187;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 187\nand sa.provider = 'salesforce';\n\n# Destination - 98870 - Destination__c\n# Stage - 79014 - StageName\n# Land Arrangement - 98856 - Land_Arrangement__c\n# Flight - 98848 - Flight__c\n# Last activity date - 98812 - LastActivityDate\n# Last modified date - 98809 - LastModifiedDate\n# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c\n# next call - 98864 - Next_Call__c\n\nselect * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\nselect * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';\nselect * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;\nselect * from activities where opportunity_id = 3538248;\n\nSELECT * FROM crm_profiles WHERE user_id = 8150;\n\nselect * from deal_risks where opportunity_id = 3538248;\n\nselect * from teams where crm_id IS NULL;\n\nSELECT opp.id AS opportunity_id,\n u.group_id AS group_id,\n MAX(\n CASE\n WHEN a.type IN (\"sms-inbound\", \"sms-outbound\") THEN a.created_at\n ELSE a.actual_end_time\n END) as last_date\nFROM opportunities opp\nleft join activities a on a.opportunity_id = opp.id\ninner join users u on opp.user_id = u.id\nwhere opp.user_id IN (9951)\n\nAND opp.is_closed = 0\nand a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL\ngroup by opp.id;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,polly.morphew@cybsafe.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 301;\nSELECT * FROM contacts WHERE id = 6612363;\nSELECT * FROM accounts WHERE id = 4235676;\nSELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;\nselect * from opportunity_stages where opportunity_id = 4503759;\n# SELECT * FROM opportunities WHERE id = 4569937;\n\nselect * from activities where crm_configuration_id = 301;\nSELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370\nSELECT * FROM participants WHERE activity_id = 26330370;\n\nSELECT * FROM teams WHERE id = 375;\nselect * from playbooks where team_id = 375;\n\nselect * from stages where crm_configuration_id = 301 and type = 'opportunity';\n\nselect * from teams;\nselect * from contact_roles;\n\nSELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';\n\nselect * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;\n\nSELECT * FROM crm_field_data WHERE object_id = 3771706;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'\nand crm_provider_id LIKE \"%traffic_light%\";\nSELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);\n\nSELECT fd.* FROM opportunities o\nJOIN crm_field_data fd ON o.id = fd.object_id\nWHERE o.team_id = 343\n# and o.user_id IS NOT NULL\nand fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)\nand fd.value != ''\norder by value desc\n# group by o.id\n;\n\nSELECT * FROM opportunities WHERE id = 3769843;\n\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, salesforce-admin@tourlane.com\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,aswini.mishra@fundingcircle.com\nSELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839\n\n\nSELECT * FROM opportunities WHERE id = 3855992;\n\nSELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988\n\nSELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';\n\nselect * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507\nSELECT * FROM crm_field_data WHERE object_id = 5874411;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379\nand sa.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, nikhil.kumar@mention-me.com\nSELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\nSELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793\nselect * from generic_ai_prompts where subject_id = 3537793;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, triger@lunio.ai\nSELECT * FROM crm_configurations WHERE id = 97;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 97;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;\nSELECT * FROM crm_fields WHERE id = 32682;\n\nselect cfd.value, o.* from opportunities o\njoin crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682\nwhere team_id = 120\nand cfd.value != ''\n;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 120\nand sa.provider = 'salesforce';\n\nselect * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';\nSELECT * FROM crm_field_data WHERE object_id = 2313439;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 410;\nSELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';\nselect * from scorecards where team_id = 410;\nselect * from scorecard_rules;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, aswini.mishra@fundingcircle.com\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\njoin users u on o.user_id = u.id\nwhere a.crm_configuration_id = 177 and a.type LIKE '%email-out%'\n# and a.actual_end_time > '2024-12-16 00:00:00'\n# and o.remotely_created_at > '2024-12-01 00:00:00'\n# and u.group_id = 1014\nand u.id = 9021\norder by a.id desc;\nSELECT * FROM opportunities WHERE id in (3981384,4017346);\nSELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);\n\nselect * from users where id = 9021;\nselect * from inboxes where user_id = 9021;\n\nselect * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';\n\nselect * from email_messages where team_id = 220\nand orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'\nand subject LIKE '%Personal%'\n# and 'from' = 'credit@fundingcircle.com'\n;\n\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\nwhere a.user_id = 9021 and a.type LIKE '%email-out%'\nand a.actual_end_time > '2024-12-18 00:00:00'\nand o.user_id IS NOT NULL\nand o.remotely_created_at > '2024-12-01 00:00:00'\norder by a.id desc;\n\nSELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;\nselect * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;\n\nselect * from team_settings where name IN ('useCloseDate');\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, jfarrell@hurree.co\nSELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 104\nand sa.provider = 'hubspot';\n\nselect * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'\nselect * from teams where crm_id IS NULL;\n\nselect t.name as 'team', u.name as 'owner', u.email, u.phone\nfrom teams t\njoin activity_providers ap on t.id = ap.team_id\njoin users u on t.owner_id = u.id\nwhere 1=1\n and t.status = 'active'\n and ap.is_enabled = 1\n# and u.status = 1\n and ap.provider = 'ms-teams';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nSELECT * FROM teams WHERE id = 442; # 14293\nselect * from users where team_id = 442;\nselect * from social_accounts sa where sa.sociable_id = 14293;\nselect * from invitations where team_id = 442;\n\n# ********************************************************************************************************\nSELECT * FROM users WHERE email LIKE '%nea.liikamaa@eletive.com%'; # 14022\nSELECT * FROM teams WHERE id = 429;\nselect * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);\nselect * from activities where opportunity_id in (4340436,4353519);\n\nselect * from transcription where activity_id IN (25630961,25381771);\nselect * from generic_ai_prompts where subject_id IN (4353519);\n\nSELECT\n a.id as activity_id,\n a.opportunity_id,\n a.type as activity_type,\n a.language,\n CONCAT(a.title, a.description) AS mail_content,\n e.from AS mail_from,\n e.to AS mail_to,\n e.subject AS mail_subject,\n e.body AS mail_body,\n p.type as prompt_type,\n p.status as prompt_status,\n p.content AS prompt_content,\n a.actual_start_time as created_at\nFROM activities a\n LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL\n LEFT JOIN email_messages e ON a.id = e.activity_id\nWHERE a.actual_start_time > '2024-01-01 00:00:00'\n AND a.opportunity_id IN (4353519)\n AND a.status IN ('completed', 'received', 'delivered')\n AND a.deleted_at IS NULL\n AND a.type NOT IN ('sms-inbound', 'sms-outbound')\nORDER BY a.opportunity_id ASC, a.id ASC;\n\nSELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293\nSELECT * FROM teams WHERE id = 442;\nSELECT * FROM crm_configurations WHERE id = 344;\nselect * from team_features where team_id = 442;\nselect * from groups where team_id = 442;\nselect * from playbooks where team_id = 442;\nselect * from playbook_categories where playbook_id = 1729;\nselect * from crm_fields where crm_configuration_id = 344 and id = 172024;\nSELECT * FROM crm_field_values WHERE crm_field_id = 172024;\nselect * from crm_layouts where crm_configuration_id = 344;\nselect * from playbook_layouts where playbook_id = 1729;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444\n\nselect s.*\n# , s.sent_at, u.name, a.*\nfrom activity_summary_logs s\ninner join activities a on a.id = s.activity_id\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 356\nand s.sent_at > date_sub(now(), interval 60 day)\norder by a.actual_end_time desc;\n\nselect * from activities a\n# inner join activity_summary_logs s on s.activity_id = a.id\nwhere a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)\n# and a.crm_provider_id is not null\n# and provider <> 'ringcentral'\nand status = 'completed'\norder by a.actual_end_time desc;\n\nselect * from teams order by id desc; # 17328, 32, 17830, integration-account@jiminny.com\nSELECT * FROM users;\nSELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active\nSELECT * FROM teams WHERE id = 260;\nselect * from team_settings where team_id = 260;\nselect * from crm_configurations where team_id = 260;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 356;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;\n\nselect * from accounts where crm_configuration_id = 221 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 221 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 221 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 221 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 221;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 221 order by id desc;\nselect * from stages where crm_configuration_id = 221 order by id desc;\n\nselect * from accounts where crm_configuration_id = 356 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 356 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 356 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 356 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 356;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 356 order by id desc;\nselect * from stages where crm_configuration_id = 356 order by id desc;\n\nselect * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)\nselect * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)\nselect * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4\nselect ce.* from calendars c\njoin users u on c.user_id = u.id\njoin calendar_events ce on c.id = ce.calendar_id\nwhere u.team_id = 260\nand (ce.start_time > '2025-02-21 00:00:00')\n;\n# calendar events 1207\n#\n\nselect * from opportunities where team_id = 260;\nSELECT * FROM crm_field_data WHERE object_id = 4696496;\n\nselect * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;\nselect * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')\n# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0\nand created_at > '2024-03-01 00:00:00'\norder by id desc; # 880 000, ringcentral, avaya\nSELECT * FROM participants WHERE activity_id = 26371744;\n\n# all activities 942 000 +\n# conference 7385 - scheduled 984 - external 343\n\nselect * from activities where id = 26321812;\nselect * from participants where activity_id = 26321812;\nselect * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);\nselect * from leads where id in (720428,689175,731546,645866,621037);\n\nselect * from users where id = 13841;\nselect * from opportunities where user_id = 9541;\nselect * from stages where id = 15900;\n\nselect * from accounts where\n# id IN (4160055,5053725,4965303,4896434)\nid in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)\n;\n\nselect * from activities where id = 26654935;\nSELECT * FROM opportunities WHERE id = 4803458;\n\nSELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;\nSELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time\nFROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);\n\nSELECT DISTINCT\n o.id, o.stage_id, s.name, a.title,\n a.*\nFROM activities a\n# INNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nINNER JOIN groups g ON u.group_id = g.id\nINNER JOIN opportunities o ON a.opportunity_id = o.id\nINNER JOIN stages s ON o.stage_id = s.id\nWHERE\n a.crm_configuration_id = 356\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 13841\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')\n AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')\n\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n )\n )\n AND (\n# s.id = 15900\n s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')\n OR s.uuid IS NULL -- Include records without opportunity stage\n )\n\nORDER BY a.actual_end_time DESC;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, willsc@leadforensics.com\nSELECT * FROM users WHERE team_id = 190;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 190\nand sa.provider = 'hubspot';\n\nselect * from role_user where user_id = 8474;\n\nselect * from crm_configurations where provider = 'bullhorn';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;\nSELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;\n\nSELECT * FROM opportunities WHERE id = 4732493;\nselect * from activities where opportunity_id = 4732493;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 443; # 358, 14315, andrea.romano@correrenaturale.com\nSELECT * FROM opportunities WHERE team_id = 443;\n\nSELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id\nFROM activities AS a\nJOIN stages AS s ON a.stage_id = s.id\nJOIN users AS u ON u.id = a.user_id\nJOIN teams AS t ON t.id = s.team_id\nWHERE u.team_id <> s.team_id and t.id > 135;\n\n\nSELECT\n crm_configuration_id,\n crm_provider_id,\n COUNT(*) as duplicate_count,\n GROUP_CONCAT(id) as stage_ids,\n GROUP_CONCAT(name) as stage_names\nFROM stages\nGROUP BY crm_configuration_id, crm_provider_id\nHAVING COUNT(*) > 1\nORDER BY duplicate_count DESC;\n\nselect * from stages where id IN (14898,14907);\n\nselect * from business_processes;\n\nSELECT *\nFROM crm_configurations\nWHERE team_id IN (\n SELECT team_id\n FROM crm_configurations\n GROUP BY team_id\n HAVING COUNT(*) > 1\n)\nORDER BY team_id;\n\nSELECT *\nFROM teams\nWHERE crm_id IN (\n SELECT crm_id\n FROM teams\n GROUP BY crm_id\n HAVING COUNT(*) > 1\n)\nORDER BY crm_id;\n\n# ***************************************************************************\nselect * from crm_configurations where provider = 'integration-app';\nSELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 andrea.romano@correrenaturale.com\nselect * from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect * from team_features where team_id = 358;\nselect * from activity_summary_logs;\n\nselect * from teams where id = 406;\n\n# ************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, srv.salesforce@sportfive.com\nselect * from activities where crm_configuration_id = 202 order by actual_end_time desc;\n\nSELECT * FROM users where id = 14637;\nSELECT * FROM teams where id = 267;\nSELECT * FROM groups where id = 1118;\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 202\n AND status IN ('completed', 'failed')\n AND recording_state != 'stopped'\n AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n AND (is_private = 0 OR user_id = 14637)\n AND (\n (\n actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n ) OR (\n actual_start_time IS NULL\n AND type IN ('sms-outbound', 'sms-inbound')\n AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tracks\n WHERE\n tracks.activity_id = activities.id\n AND tracks.type IN ('audio', 'video')\n )\nORDER BY actual_end_time DESC;\n\nSELECT DISTINCT\n a.*\nFROM activities a\nINNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nWHERE\n a.crm_configuration_id = 202\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 14637\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND a.user_id = 14637\n )\n )\n\nORDER BY a.actual_end_time DESC\n;\n\nSELECT DISTINCT a.*\nFROM activities a\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams t ON u.team_id = t.id\n# INNER JOIN tracks tr ON a.id = tr.activity_id\n# INNER JOIN groups g ON u.group_id = g.id\nWHERE 1=1\n AND t.id = 267\n# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND tr.type NOT IN ('audio', 'video')\n AND (\n a.is_private = 0\n OR a.user_id = 14637\n )\n AND (\n (a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')\n OR (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'\n )\n )\n# and NOT EXISTS (\n# SELECT 1\n# FROM tracks t\n# WHERE t.activity_id = a.id\n# AND t.type IN ('audio', 'video')\n# )\n\nORDER BY a.actual_end_time DESC;\n\nSELECT * FROM tracks WHERE activity_id = 26485995;\n\nselect a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 202\n# and a.is_internal = 0\nand (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type IN (\"softphone\",\"softphone-inbound\",\"conference\",\"sms-inbound\")\nand a.status IN ('completed', 'failed')\n# and a.external_id is not null\norder by a.actual_end_time desc;\n\nselect * from activities a where a.crm_configuration_id = 202\nand a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'\n# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_field_data WHERE crm_field_id = 98809;\n\nselect * from users where status = 1 AND timezone = 'MDT';\n\nselect * from opportunities where id = 3769814;\nselect * from deal_risks where opportunity_id = 3769814;\n\nselect cp.* from crm_profiles cp\njoin users u on cp.user_id = u.id\njoin crm_configurations crm on cp.crm_configuration_id = crm.id\nwhere crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';\n\nselect * from crm_fields where id = 154575;\n\nselect * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';\nSELECT * FROM teams WHERE id = 176; # crm 148\nselect * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nselect * from crm_fields cf\njoin crm_configurations crm on crm.id = cf.crm_configuration_id\nwhere crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');\n\n# *********************************************************************************************\nSELECT * FROM users WHERE id IN (15415, 15418);\nSELECT * FROM groups WHERE id IN (1805,1806);\nSELECT * FROM playbooks WHERE id = 1860;\nSELECT * FROM playbook_categories WHERE id = 38634;\nSELECT * FROM crm_fields WHERE id = 189962;\n\nSELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 raza.gilani@vuelio.com\n\nSELECT * FROM crm_profiles WHERE user_id = 15415;\nSELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';\n\nselect * from sidekick_settings where team_id = 472;\n\nSELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418\nSELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415\n\n# *********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, salesforce-integrations@teamtailor.com\nselect * from crm_configurations where id = 218;\nSELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765\nSELECT * FROM users WHERE id IN (13232, 13230);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n0057R00000EPL5HQAX Inez Ekblad\n\n1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur\n\nSELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);\n\n############################################################################################\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id IN (94491,94493,94498);\nSELECT * FROM users WHERE id = 13658;\nSELECT * FROM teams WHERE id = 109;\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, katy.holden@strengthscope.comk\nSELECT * FROM stages WHERE crm_configuration_id = 390;\nselect * from business_processes where team_id = 481 and crm_configuration_id = 390;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 481\nand sa.provider = 'salesforce';\n\n\nSELECT * FROM users WHERE id = 15780; # team 462\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 462\nand sa.provider = 'hubspot';\n\n\nselect * from teams where id = 495;\nSELECT * FROM users WHERE id = 15794;\nselect * from social_accounts where sociable_id = 15794;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752\nSELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794\nSELECT * FROM activities WHERE crm_configuration_id = 407\nand status = 'completed' and type = 'conference'\norder by id desc;\n\nselect ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id\njoin permission_role pr on pr.role_id = ru.role_id\n join permissions p on p.id = pr.permission_id\nwhere team_id = 495 and p.name IN ('dial');\n\nselect * from permission_role;\n\nselect * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;\nSELECT * FROM activities WHERE id = 29512773;\nSELECT * FROM activities WHERE id IN (29042721,28991325,29002874);\n\nSELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 407\n# and a.id IN (29042721,28991325,29002874);\n\nSELECT * FROM users WHERE id = 15794;\nSELECT * FROM users WHERE team_id = 495;\nSELECT * FROM social_accounts WHERE sociable_id = 15794;\nSELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';\nSELECT * FROM contacts WHERE team_id = 495;\nSELECT * FROM leads WHERE team_id = 495;\nSELECT * FROM accounts WHERE team_id = 495;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 407;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 407;\nSELECT * FROM crm_configurations WHERE id = 407;\nSELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'\nand user_id IS NOT NULL and is_closed = 1 and is_won = 1;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103\nSELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064\nSELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');\n\n# *********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 325\nand sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085\nSELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733\nSELECT * FROM activity_summary_logs where activity_id = 28719733;\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444\nSELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';\nSELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630\nselect * from activities where crm_configuration_id = 356 and lead_id = 841732;\n\nSELECT * from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 356;\n\nselect * from activities where crm_configuration_id = 356\nand actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'\norder by id desc;\n\nselect * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;\nselect * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\n\nselect * from team_features where team_id = 260;\nselect * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;\n\nselect * from crm_fields;\nselect * from crm_layout_entities;\n\nSELECT * FROM teams WHERE name LIKE '%Optable%';\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id in (94491,94493,94498);\n\nselect * from teams where crm_id IS NULL;\n\nSELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;\n\n# *************************************************************************************************\nselect * from team_domains where team_id = 399;\nSELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207\n\nselect * from calendar_events where id = 5163781;\nSELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896\nSELECT * FROM participants WHERE activity_id = 29443896;\nselect * from contacts where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\nselect * from leads where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\n\nselect * from activities where user_id = 14937 order by created_at ;\n\nselect * from users where id = 14937;\n\nselect * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';\nselect * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';\n\nselect * from activities a join participants p on a.id = p.activity_id\nwhere crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';\n\n# *************************************************************************************************\nSELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';\nSELECT * FROM opportunities WHERE team_id = 379 order by id desc;\nSELECT * FROM teams WHERE id = 379;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379 and sociable_id = 13852\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE id = 307;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 307;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307\n and id IN (144750,144855,145158,155227);\n\nSELECT * FROM activities;\n\n\nselect * from activities\nwhere created_at > '2025-07-01 00:00:00'\n# and created_at < '2025-08-01 00:00:00'\nand type not in ('email-outbound', 'email-inbound')\nand account_id is null\nand contact_id is null\nand lead_id is null\nand opportunity_id is not null\n;\nSELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);\nSELECT * FROM crm_configurations WHERE id in (335,301,200);\n\nselect * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';\n\nSELECT * FROM teams WHERE name LIKE '%Resights%';\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nselect * from teams where id IN (442);\n\nselect * from activities\nwhere crm_configuration_id = 177\nand provider = 'amazon-connect'\n order by id desc;\n# and source <> 'gong';\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nSELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;\n\n\nselect * from crm_configurations where store_transcript = 1;\nSELECT * FROM teams WHERE id IN (80);\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 277\nand sa.provider = 'salesforce';\n\nselect * from activities where crm_configuration_id = 213 and account_id = 2511502;\n\nselect * from crm_configurations where id = 213;\n\nSELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604\nSELECT * FROM participants WHERE activity_id = 33981604;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 431\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223\nselect * from activity_summary_logs where activity_id = 33997223;\nselect * from activity_notes where activity_id = 33997223;\n\n# ***********************************\nSELECT * FROM teams WHERE name LIKE '%Abode%';\n\n\nselect * from features;\nselect * from teams t\nwhere t.status = 'active'\nand id NOT IN (select team_id from team_features where feature_id = 9)\n;\n\n\nselect * from playbook_layouts where playbook_id = 1725;\nSELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473\nselect * from teams where id = 318;\nselect * from crm_configurations where team_id = 318;\nselect * from playbooks where team_id = 318;\nSELECT * FROM crm_layouts where crm_configuration_id = 381;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;\nSELECT * FROM crm_fields WHERE id IN (192938,192936,192939);\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;\nSELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);\n\nSELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533\n\n\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nSELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);\n\nSELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124\nselect * from crm_layouts where crm_configuration_id = 281;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nselect * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);\nselect * from opportunities where crm_configuration_id = 281;\n\nSELECT * FROM activities WHERE id IN (34211315, 34130075);\nSELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);\n\nselect cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd\njoin crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id\njoin crm_fields cf on cle.crm_field_id = cf.id\nwhere cf.deleted_at IS NOT NULL\nGROUP BY cle.id, cf.id;\n\nselect * from crm_layouts where id IN (355);\nselect u.email, t.crm_id, t.* from teams t\njoin users u on u.id = t.owner_id\nwhere crm_id IN (97);\n\nSELECT * FROM crm_fields WHERE id = 96492;\n\nselect * from permissions;\nselect * from permission_role where permission_id = 247;\nselect * from roles;\n\nselect * from migrations;\n# *****************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('291e3c21-11cc-4728-aee7-6e4bedf86d72') = uuid; # 34262174\nSELECT * FROM crm_configurations WHERE id = 301;\nSELECT * FROM teams WHERE id = 343;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from participants where activity_id = 34262174;\n\nselect * from contacts where crm_configuration_id = 301 and id = 6976326;\nselect * from accounts where crm_configuration_id = 301 and id IN (4647626, 4815829); # 30761335403\n\nselect * from activity_summary_logs where activity_id = 34262174;\n\nselect * from users where status = 1 AND timezone = 'EST';\n\n# ****************************************************************************\nSELECT * FROM users WHERE id = 13869;\nSELECT * FROM crm_configurations WHERE id = 320;\nSELECT * FROM teams WHERE id = 401;\n\nSELECT * FROM activities WHERE uuid_to_bin('2228c16f-10be-48d5-90d4-67385219dc01') = uuid; # 29670601\n\nSELECT * FROM accounts WHERE id = 7761483;\nSELECT * FROM opportunities WHERE id = 6051814;\n\nSELECT * FROM teams WHERE name LIKE '%Seedlegals%';\n\n;select * from opportunities where updated_at > '2025-10-11' AND crm_provider_id = '34713761166';\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 177;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 577;\nSELECT * FROM crm_fields WHERE id IN (68458,68459,68480,68497,68524,68530,68554,68618,68662,68781,68810,68898,68981,69049,97467);\n\nSELECT t.id, crm.id, t.name, crm.sync_objects, crm.provider, crm.last_synced_at FROM crm_configurations crm join teams t on t.crm_id = crm.id\nwhere t.status = 'active' AND crm.provider = 'hubspot' AND crm.last_synced_at < '2025-10-22 00:00:00';\n\nSELECT * FROM activities WHERE uuid_to_bin('fa09449f-cba9-496a-b8f3-865cd3c72351') = uuid;\nSELECT * FROM crm_configurations where id = 184;\nSELECT * FROM teams WHERE id = 246;\nSELECT * FROM social_accounts WHERE sociable_id = 9259 and provider = 'hubspot';\n\nSELECT * FROM users WHERE email LIKE '%rhian.old@bud.co.uk%'; # 17700\nSELECT * FROM teams WHERE id = 551;\n\nSELECT * FROM crm_configurations WHERE id = 471;\nSELECT * FROM activities WHERE crm_configuration_id = 471 and crm_provider_id IS NOT NULL;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 471;\nSELECT * FROM crm_fields WHERE id = 307260;\nSELECT * FROM crm_field_values WHERE crm_field_id = 307260;\n\nselect * from crm_layouts where crm_configuration_id = 471;\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1547;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1548;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 551 and sa.provider = 'hubspot';\n\nSELECT * FROM teams WHERE name LIKE '%$PCS%';\n\n# ********************************************************************************************************\nselect * from crm_configurations crm\njoin teams t on t.crm_id = crm.id\nwhere t.status = 'active'\nand crm.provider = 'hubspot';\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 crm_configurations where id = 331; # 416\nSELECT * FROM teams WHERE id = 416;\nSELECT * FROM opportunities WHERE team_id = 190;\n\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%';\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 190 and sa.provider = 'hubspot';\n\n\n\nSELECT * FROM teams WHERE name LIKE '%Rapaport%'; # 431, 337\nSELECT * FROM teams where id = 431;\nSELECT * FROM crm_configurations where team_id = 431;\nSELECT * FROM activity_providers where team_id = 431;\nSELECT * FROM activities where crm_configuration_id = 337 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 431 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%BiP%'; # 401, 320\nSELECT * FROM teams where id = 401;\nSELECT * FROM crm_configurations where team_id = 401;\nSELECT * FROM activity_providers where team_id = 401;\nSELECT * FROM activities where crm_configuration_id = 320 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 401 and sa.provider = 'salesforce';\n\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 307; # 379 - Story Terrace Inc , portalId: 3921157\nSELECT * FROM contacts WHERE team_id = 379 and updated_at > '2026-01-31 11:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 379 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; # 563 - LATUS Group (ad94d501-5d09-44fd-878f-ca3a9f8865c3) , portalId: 3904501\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 338; # 432 - Formalize , portalId: 9214205\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 432 and sa.provider = 'hubspot';\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 436; # 519 - Moxso , portalId: 25531989\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 96; # 119 - Nourish Care , portalId: 26617984\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 331; # 416 - The National College , portalId: 7213852\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 308; # 380 - Foodles , portalId: 7723616\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 379; # 471 - imat-uve , portalId: 9177354\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 465; # 545 - Spotler , portalId: 144759271\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 455; # 537 - indevis , portalId: 25666868\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 200; # 265 - Jobadder , portalId: 6426676\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 335; # 429 - Eletive , portalId: 6110563\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 363; # 456 - Global Group , portalId: 8901981\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 297; # 369 - Unbiased , portalId: 9229005\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 353; # 449 - Fuuse , portalId: 25781745\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 487; # 566 - Nimbus , portalId: 39982590\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 487;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1630;\nselect * from crm_fields where crm_configuration_id = 487 and\n(uuid_to_bin('4c6b2971-64d4-45b8-b377-427be758b5a5') = uuid or uuid_to_bin('59e368d8-65a0-4b77-b611-db37c99fbe68') = uuid);\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 420; # 506 - voiio , portalId: 145629154\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 479; # 558 - Momice , portalId: 535962\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 59; # 80 - Storyclash GmbH , portalId: 4268479\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 175; # 203 - Team iAM , portalId: 5534732\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 368; # 460 - OneTouch Health , portalId: 5534732183355\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\n\n\nselect * from users where id = 29643;\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM teams WHERE name LIKE '%Buynomics%'; # 462, 482, 14910\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\n# and description like '%The call focused on understanding Welch%'\norder by id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 462 and sa.provider = 'salesforce';\n\nselect * from contacts where crm_configuration_id = 482 and name = 'Cyndall Hill'; # 15504749\nselect * from contacts where id = 10891096; # 482\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\nand contact_id = 15504749\norder by id desc;\n\nselect * from activities where id = 36793003; # 96cc7bc1-8622-4d27-92f4-baf664fc1a56, 00UOf00000PDdOXMA1\nselect * from transcription where id = 7646782;\nselect * from ai_prompts where transcription_id = 7646782;\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7a8471a3-847e-4822-802b-ddf426bbc252') = uuid; # 37370018\nSELECT * FROM activity_summary_logs WHERE activity_id = 37370018;\nSELECT * FROM teams WHERE id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 555 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7c17b8aa-09df-4f85-a0f7-51f47afd712d') = uuid; # 37395250\nSELECT * FROM activities WHERE uuid_to_bin('14d60388-260d-494b-aa0d-63fdb1c78026') = uuid; # 37395250\n\nSELECT a.* FROM activities a JOIN crm_configurations c on c.id = a.crm_configuration_id\nwhere a.type IN ('softphone', 'softphone-outbound') and c.provider = 'hubspot'\nand a.provider NOT IN ('hubspot')\n# and a.provider IN ('salesloft')\n# and c.id NOT IN (70)\n# and a.duration > 30\n# and actual_start_time > '2026-02-05 00:00:00'\norder by a.id desc;\n\nSELECT * FROM activities WHERE id = 37549787;\nSELECT * FROM crm_profiles WHERE user_id = 17613;\n\nSELECT * FROM crm_configurations WHERE id = 70;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 93 and sa.provider = 'hubspot';\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations WHERE id = 373; # KPSBremen.de 465 # - no social account\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 465 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 494;\n\nSELECT * FROM teams WHERE name LIKE '%splose%'; # 572, 495, 18708\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 572 and sa.provider = 'pipedrive';\n\nselect * from opportunities where team_id = 572\n# and name like '%Onebright%'\n# and is_closed = 1 and is_won = 0\n order by id desc;\n\n\nselect * from users where deleted_at is null and status = 2;\n\nselect * from contacts where id = 17900517;\nselect * from accounts where id = 10109838;\nselect * from opportunities where id = 6955880;\n\nselect * from opportunity_contacts where opportunity_id = 6955880;\nselect * from opportunity_contacts where contact_id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nSELECT * FROM activities WHERE uuid_to_bin('adcb8331-5988-4353-834e-383a355abba2') = uuid; # 38056424, crm 104659682404\nselect * from teams where id = 456;\nSELECT * FROM crm_configurations WHERE id = 363;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 456 and sa.provider = 'hubspot';\n\nselect * from crm_layouts where crm_configuration_id = 363;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (1203, 1204, 1635);\nSELECT * FROM crm_fields WHERE id IN (181536, 181538, 213455);\n\nSELECT * FROM teams WHERE name LIKE '%Electric%'; # 342, 272, 12767\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and name like 'NORTHUMBRIA POL%'; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 order by remotely_created_at asc; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and updated_at > '2026-01-01 00:00:00';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 272 and object_type = 'opportunity';\nSELECT * FROM crm_field_values WHERE crm_field_id = 127164;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\n\nSELECT * FROM teams WHERE id = 472;\nSELECT * FROM crm_configurations WHERE id = 380;\nselect * from activities where id = 38285673; # 38285673\nSELECT * FROM users WHERE id = 16942;\nSELECT * FROM groups WHERE id = 1964;\nSELECT * FROM playbooks WHERE id = 2033;\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 499; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1678;\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\n\nSELECT * FROM activities WHERE uuid_to_bin('96b1261f-2357-49f9-ab38-23ce12008ea0') = uuid;\n\nselect * from contacts c\nwhere c.crm_configuration_id = 370 order by c.updated_at desc;\n\nSELECT * FROM participants where activity_id = 38833541;\nSELECT * FROM participants where activity_id = 39216301;\nSELECT * FROM activity_summary_logs where activity_id = 39216301;\nSELECT * FROM activities WHERE uuid_to_bin('c7d99fbe-1fb1-41f2-8f4d-52e2bf70e1e9') = uuid; # 38833541, crm 478116564181\nSELECT * FROM activities WHERE uuid_to_bin('2e6ff4d3-9faa-447a-a8c1-9acde4d885ae') = uuid; # 39216301, crm 480171536586\nselect * from crm_profiles where crm_configuration_id = 319 and crm_provider_id = 525785080;\nselect * from opportunities where crm_configuration_id = 319 and crm_provider_id = 410150124747;\nselect * from accounts where crm_configuration_id = 319 and crm_provider_id = 47150650569;\nselect * from contacts where crm_configuration_id = 319 and crm_provider_id IN ('665587441856', '742723347700');\n# owner 13236 525785080\n# contact 1 16779180 665587441856 - activity - Alex Howes alex@supportroom.com created 2026-01-26\n# contact 2 19247563 742723347700 - ash@supportroom.com 2026-03-24\n# company 4176133 47150650569\n# deal 7100953 410150124747\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 400 and sa.provider = 'hubspot';\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556; # owner: 18101, crm: 477\nselect * from crm_configurations where id = 477;\nSELECT * FROM users WHERE id = 18101;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'integration-app';\n\nselect * from opportunities where id = 7594349;\nselect * from opportunity_stages where opportunity_id = 7594349 order by created_at desc;\nselect * from business_processes where id = 6024;\nselect * from business_process_stages where stage_id = 16352;\nselect * from business_process_stages where business_process_id = 6024;\nselect * from stages where team_id = 459;\nselect * from teams where id = 459;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 459 and sa.provider = 'hubspot';\n\nSELECT os.stage_id, s.crm_provider_id, s.name, COUNT(*) as cnt\nFROM opportunity_stages os\nJOIN stages s ON s.id = os.stage_id\nWHERE os.opportunity_id = 7594349\nGROUP BY os.stage_id, s.crm_provider_id, s.name\nORDER BY cnt DESC;\n\nSELECT s.id, s.crm_provider_id, s.name, s.team_id, s.crm_configuration_id\nFROM stages s\nJOIN business_process_stages bps ON bps.stage_id = s.id\nWHERE bps.business_process_id = 6024\nAND s.crm_provider_id = 'contractsent';\n\nselect * from stages where id IN (16352,20612,18281,7344,16378,16309,5036,15223,14535,6293,12098,11607)\n\nSELECT * FROM teams WHERE name LIKE '%Pulsar Group%'; # 472, 380, 15138, raza.gilani@vuelio.com\nselect * from playbooks where team_id = 472; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 2288;\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 380;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 472 and sa.provider = 'salesforce';\n\nselect * from activities where id = 58081273;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2;\n\nSELECT * FROM users WHERE name LIKE '%Neil Hoyle%'; # 17651\nSELECT * FROM social_accounts WHERE sociable_id = 17651;\n\nSELECT * FROM activities WHERE uuid_to_bin('975c6830-7d49-4c1e-b2e9-ac80c10a738a') = uuid;\nSELECT * FROM opportunities WHERE id IN (7842553, 6211727);\nSELECT * FROM contacts WHERE id IN (10202724, 6211727);\nSELECT * FROM opportunity_stages WHERE opportunity_id = 7842553;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 519 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 436;\nselect * from crm_profiles where crm_configuration_id = 436; # 76091797 -> 16612\n\nselect * from contact_roles where contact_id = 10202724;\n\nselect * from stages where team_id = 519; # 18778\n18775\n\nSELECT\n id,\n crm_provider_id,\n stage_id,\n is_closed,\n is_won,\n stage_updated_at,\n updated_at\nFROM opportunities\nWHERE id IN (6211727, 7842553);\n\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id = 6211727 AND contact_id = 10202724;\n\nSELECT id, name, stage_id, is_closed, is_won, updated_at, remotely_created_at\nFROM opportunities\nWHERE account_id = 8179134\nORDER BY updated_at DESC;\n\n\nselect * from text_relays where created_at > '2026-01-01';\nAND id IN (691, 692);\n\nselect * from teams;\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = a.user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nSELECT DISTINCT u.id, u.email, u.name, u.team_id, t.name as team_name,\n t.twilio_sms_sid, t.twilio_messaging_sid\nFROM users u\nINNER JOIN teams t ON u.team_id = t.id\nWHERE (t.twilio_sms_sid IS NOT NULL OR t.twilio_messaging_sid IS NOT NULL)\nAND u.status = 1\nORDER BY t.name, u.email;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 187 and sa.provider = 'salesforce';\n\nselect * from activities where id = 31264367;\nselect * from contacts where id = 6331639;\nselect * from accounts where id = 4156632;\nselect * from opportunities where id = 4843610;\n# update `activities` set `account_id` = 4156632, `contact_id` = 6331639, `opportunity_id` = 4843610,\n# `stage_id` = 13273, `activities`.`updated_at` = 2026-05-22 07:16:17 where `id` = 31264367)\"\n\nselect * from text_relays where created_at > '2026-05-01';\n\nselect * from activities order by id desc;\n\nselect * from users where name like '%Subra%';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('04a9cfad-2c87-4453-9e72-20aeb78ccf8d') = uuid;\nselect * from teams where id = 555;\nselect * from stages where team_id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 100 and sa.provider = 'hubspot';\n\nselect id, is_closed, is_won, stage_updated_at, crm_provider_id, stage_id, probability,\nclose_date, forecast_category, deleted_at, created_at, remotely_created_at, updated_at\nfrom opportunities where team_id = 555 and stage_id = 20616 order by updated_at desc limit 10;\n\nselect id, crm_provider_id, name, probability, updated_at, deleted_at\nfrom stages\nwhere id = 20616;","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}]...
|
7945821021870316922
|
-537191020546090905
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
1
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'ac...
|
88389
|
NULL
|
NULL
|
NULL
|
|
88389
|
3013
|
54
|
2026-05-28T17:01:02.382888+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987662382_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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":"Show Replace Field","depth":4,"bounds":{"left":0.11868351,"top":0.15482841,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.13131648,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1634008765766868483
|
-8382957821725799484
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-lHubspotClientinterface.ph© HubspotTokenManager.pt© PayloadBuilder.phpoKimol CimosewhooResponseNormalize.pho© SyncFieldAction.ohoCSWnCKealCohe wWwK© WebhookSvncBatchProcelisteners> MetadataaMicrationP oedriveEh SalesforceafeldsaOpoortunityVatchenOpportunitySyncStrategyProsoectSaarchstrateosametiteC DecorateActivity.php( DeletcObiectsTrait.phpoewanarinithooe nha© PayloadBuilder.phpc) Profile.phpe QueryBulderonp© QueryHandler.phpeQuerviterator.oh© QueryResults.php© Service.php© SyncBatchRedisService.pth TraitsC BaseClient ohoCrmActivityProviderinteorateCCnlACMiWMoh©rmobiectsteso wer.onoC. DefaultProsoectSearchStrateC mallteloer.ond3Findeproscectinterace.onoC) LavouMansoe onoexisiinastaad10.02.23 VasilevDaeosnszolGraham811192.04.18Graham190218 Graham4.05.26405284.05.26A05284.05.262.10.253.10.2512.10-254.05.264.05.264.05.264.05.264.05.26405.2540525405.2617.02.25 iliar4.05.26Matchdomsinsysma inteaneC Opportun tvActvitwlatcheee2A419Graham10.02 18 Grabameennortur wewn.Ctestomed20419Grahamrnenont tschd nhrHe OrnenontCostrhSrond nhnGraham100516 CrkamAottylwiew outrenuaet todayRay422424426428430439442451457sraveloe© RecordSelector.phgC) ACUVIY.or.pC) Team.phd# HS local [liminny@localhostA console (EU) x iii users (EU)(© HubspotLastModifiedCreatedRecentlyOpenSyncStrategy.phcconsole (STAGINGclass Service extends BaseService implements01 A7 A149 V1V33 /1 A v 170%public function importStages(?array Stypes = null, ?string SnissingStageNane = null): ?Stage1706—1708crn_provider_1d => Spl'1d"J1, thssosrearosdeteondoseesrwdthisoahe"e'1s_selectable'=> Spl'active"]buesnecs anocecs a' =s ShussnessProcessc5d 22 mulil1):Stages - fetch all existing stages upfront to avoid N+1 queriesxistingStages = Sthis->config-›stages->withTrashedOanchetwoeStage:: TYPE._OPPORTUNITY,foreach (Spl"stages' as SdealStage) 4$s = ResponseNornalize::normalizeDeal Stage(SdealStage)/** @vac ?Stage SexistingStage */SexistingStage = SexistingStases-saetSsd0// Restore soft-deleted stages that are now active in HubSpotif (SexistinoStage?->trashedO &s Ssf'actsive' 1)losert shace mndanes corrodelared necords withoun rastonsno thentSstage = Sthis->config->stages()->withTrashed()->update0rCreateClMmeostasrosd=> ch staseudath(sefilabel !1lwidth: 58).eraedrhselnhe""yne"=> Stage:: TYPE_OPPORTUNITY,=> $s['display0rder'].1e colectahla=> $s['active']tnrahshsl4tw,=> $s['probability') * 100171612111— 1715-1728= 1726=172- 1722=172-172)=173017391E1732—1733-173581736E17571738= 17551746=1742=1742=114-17451746 %Tx: AutovSo liminnyvBROER PYTnane, M.emare031 A9 A29 V3 /109 A VSELECT * FRON tEaNS WHERE name LIKE "Stounlanes: # 187, 289, 8158SEEiNTCONCAT(U.1d, CASE WHEN U.10 = t.ouner_1d THEN" (owner)" ELSEMrenasisa.*t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.soclable_idJOIN teans t 1.nc->1: on t.id = u.team_1dWHERE u.tean_id = 187 and sa.provider = 'salesfonce':select * fron activities where id = 31264367select * fron contacts where id = 6331639:Belecekx tron accoun ancheo- 450054select * fron opportunities where id = 4843610:#Uodare#"stage_id' = 132730c0un8-4300-4"contact_id' ="updated.at" = 2826-95-22 07:16:select * fron text relavs where created at > '2926-95-91°:select * fron actviates order oy 1d desc;select * fron users where nane tuike "subraaSELECT * FROM opportunities WHERE unid to_bin('04a9cfad-2c87-4453-$select * fron teans where 1d= 555%select * fron stages where tean_1d = 555:SETSTMTCONCAT(U.Ld, CASE WHEN U.1d = t.ouner_id THEN" (owner)" ELSEsa.*,onnenstil socal nccounteeJOIN users u on u.id = sa.sociable_idahittinnetneosonettsWHERE u.tean_id = 100 and sa.provider = 'hubspot":select id, is_closed, is_won, stage_updated.at, crm_provider_id, stclose date, forecast_category, deleted_at, created_at, remotely_crtfron opportunities where tean id = SSS and stage_id = 28616 orderselect sid, ern-provider id, nane, probability, undated at, deletos:—1/40fron stageswhere id = 28616$f (SnissingStageNane aas Ssfid'n ‹SmissinaStage = Sstage:ServiceTestCascadeles Orcnnworeebeeionhomwstotino oosoa/huospot/Service.php b/app/Services/Cra/Hubspot/Service.phgNow let me look at the Nov 2025 bulk import - what importStages looked like at that time:• git log -oneline -before="2025-11-19" --after="2025-09-01" -- app/Services/Crm/Hubspot/Service-php | head -Sbod is vertertaskexcutd eeseptionsretry on unexpected exceptionsair shon slcteas, — aoo/eruiicesherHunsoor/Service,oho lared eA "eoorraoss.• qát show 516729105b:a00/Services/Crm/Hubspot /Service,pho | qreo -A 59 "function iecortStaoes"publie function inportstages(zarray Stypes = null, Zstring SaissingStageNane = null): 75tage// Use the HubSpot API client instead of the SDK crmPipelines() methoSoloeiinesResponse e SthisesCLtent-soetInstance()-sgetCLient ()-srequest ('GST', Sendooint)soloelines = spfoelinesResponse-sdata-sresults:.• gít shơw 5fd720fd5b:app/Services/Crm/Hubspot/Service.php | grep -A 70 "function importStages" | tail -2.'cr provider id' = ss('id')tean_1dHleutt e 1odThe probability logic was identical in Nov 2025. Now let me check normalizeDealStage at that time0 git show 5fd720fdSb:app/Services/Crm/Hubspot/ResponseNormalize.php | grep -A 20 "normalizeDealStage)rt aeuthientuniinu Lo mey koulu+0.Kwweuname(Run 33-) (Skip• Ot4 spag...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
88388
|
3012
|
40
|
2026-05-28T17:01:02.276726+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987662276_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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":"ServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ServiceTest'","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}]...
|
1101258516708334978
|
-9211620050090884732
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:01:018 10Untitled +...
|
88385
|
NULL
|
NULL
|
NULL
|
|
88387
|
3013
|
53
|
2026-05-28T17:00:56.745961+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987656745_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
1
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'ac...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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":"Show Replace Field","depth":4,"bounds":{"left":0.11868351,"top":0.15482841,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.13131648,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"existingStages","depth":4,"bounds":{"left":0.14228724,"top":0.15403032,"width":0.043882977,"height":0.015961692},"on_screen":true,"value":"existingStages","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.19514628,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.20511968,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.2137633,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.22240691,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/2","depth":4,"bounds":{"left":0.23603724,"top":0.15323225,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.26163563,"top":0.15243416,"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":"Next Occurrence","depth":4,"bounds":{"left":0.27027926,"top":0.15243416,"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":"Filter Search Results","depth":4,"bounds":{"left":0.2789229,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.28756648,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.46210107,"top":0.15243416,"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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.39727393,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.40658244,"top":0.18355946,"width":0.0076462766,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"149","depth":4,"bounds":{"left":0.4162234,"top":0.18355946,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.43018618,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.43949467,"top":0.18355946,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.45179522,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.46077126,"top":0.1819633,"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.4680851,"top":0.1819633,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse HubSpot\\Client\\Crm\\Owners\\Model\\PublicOwner;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->handleOwnersApiException($e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n $profile = $this->processOwner($owner, $teamRepository, $profileRepository);\n\n if ($profile && $userToSearch && $userToSearch->getId() === $profile->getUserId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function handleOwnersApiException(\\HubSpot\\Client\\Crm\\Owners\\ApiException $e): never\n {\n $statusCode = $e->getCode();\n $errorMessage = $e->getMessage();\n $responseBody = $this->parseResponseBody($e->getResponseBody());\n\n $isPermissionError = $this->isPermissionError($statusCode, $errorMessage, $responseBody);\n\n $logContext = [\n 'team_id' => $this->team->getId(),\n 'team_uuid' => $this->team->getUuid(),\n 'config_id' => $this->config->getId(),\n 'status_code' => $statusCode,\n 'error_message' => $errorMessage,\n 'response_body' => $responseBody,\n ];\n\n if ($isPermissionError) {\n $this->logPermissionError($logContext);\n } else {\n $this->logger->error('[HubSpot] Could not sync the profiles.', $logContext);\n }\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n private function parseResponseBody(?string $rawBody): ?array\n {\n if ($rawBody === null || $rawBody === '') {\n return null;\n }\n\n try {\n return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n } catch (\\JsonException) {\n return null;\n }\n }\n\n private function logPermissionError(array $logContext): void\n {\n $this->logger->critical(\n '[HubSpot] ⚠️ PERMISSION ERROR: Cannot sync profiles - Missing OAuth scopes',\n array_merge($logContext, [\n 'action_required' => 'Request additional HubSpot OAuth scopes',\n 'required_scope' => 'crm.objects.owners.read',\n 'impact' => 'Opportunities will have owner_id but NO user_id - AI automation and reporting will fail',\n 'resolution' => 'Manually request scope from HubSpot account admin or re-authenticate',\n ])\n );\n }\n\n private function processOwner(\n PublicOwner $owner,\n TeamRepository $teamRepository,\n ProfileRepository $profileRepository\n ): ?Profile {\n if ($owner->getArchived()) {\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n return null;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n return null;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n if (! $user instanceof User) {\n return null;\n }\n\n return $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n }\n\n private function isPermissionError(int $statusCode, string $errorMessage, ?array $responseBody): bool\n {\n // Check HTTP 403 Forbidden\n if ($statusCode === 403) {\n return true;\n }\n\n // Check for scope-related keywords in error message\n $scopeKeywords = ['scope', 'permission', 'forbidden', 'unauthorized', 'access denied', 'oauth'];\n $lowerErrorMessage = strtolower($errorMessage);\n\n foreach ($scopeKeywords as $keyword) {\n if (str_contains($lowerErrorMessage, $keyword)) {\n return true;\n }\n }\n\n // Check response body for scope errors\n if ($responseBody !== null) {\n return $this->arrayContainsKeyword($responseBody, $scopeKeywords);\n }\n\n return false;\n }\n\n /**\n * Recursively search array for keywords in values\n */\n private function arrayContainsKeyword(array $data, array $keywords): bool\n {\n foreach ($data as $value) {\n if (is_array($value)) {\n if ($this->arrayContainsKeyword($value, $keywords)) {\n return true;\n }\n } elseif (is_string($value)) {\n $lowerValue = strtolower($value);\n foreach ($keywords as $keyword) {\n if (str_contains($lowerValue, $keyword)) {\n return true;\n }\n }\n }\n }\n\n return false;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse HubSpot\\Client\\Crm\\Owners\\Model\\PublicOwner;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->handleOwnersApiException($e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n $profile = $this->processOwner($owner, $teamRepository, $profileRepository);\n\n if ($profile && $userToSearch && $userToSearch->getId() === $profile->getUserId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function handleOwnersApiException(\\HubSpot\\Client\\Crm\\Owners\\ApiException $e): never\n {\n $statusCode = $e->getCode();\n $errorMessage = $e->getMessage();\n $responseBody = $this->parseResponseBody($e->getResponseBody());\n\n $isPermissionError = $this->isPermissionError($statusCode, $errorMessage, $responseBody);\n\n $logContext = [\n 'team_id' => $this->team->getId(),\n 'team_uuid' => $this->team->getUuid(),\n 'config_id' => $this->config->getId(),\n 'status_code' => $statusCode,\n 'error_message' => $errorMessage,\n 'response_body' => $responseBody,\n ];\n\n if ($isPermissionError) {\n $this->logPermissionError($logContext);\n } else {\n $this->logger->error('[HubSpot] Could not sync the profiles.', $logContext);\n }\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n private function parseResponseBody(?string $rawBody): ?array\n {\n if ($rawBody === null || $rawBody === '') {\n return null;\n }\n\n try {\n return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n } catch (\\JsonException) {\n return null;\n }\n }\n\n private function logPermissionError(array $logContext): void\n {\n $this->logger->critical(\n '[HubSpot] ⚠️ PERMISSION ERROR: Cannot sync profiles - Missing OAuth scopes',\n array_merge($logContext, [\n 'action_required' => 'Request additional HubSpot OAuth scopes',\n 'required_scope' => 'crm.objects.owners.read',\n 'impact' => 'Opportunities will have owner_id but NO user_id - AI automation and reporting will fail',\n 'resolution' => 'Manually request scope from HubSpot account admin or re-authenticate',\n ])\n );\n }\n\n private function processOwner(\n PublicOwner $owner,\n TeamRepository $teamRepository,\n ProfileRepository $profileRepository\n ): ?Profile {\n if ($owner->getArchived()) {\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n return null;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n return null;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n if (! $user instanceof User) {\n return null;\n }\n\n return $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n }\n\n private function isPermissionError(int $statusCode, string $errorMessage, ?array $responseBody): bool\n {\n // Check HTTP 403 Forbidden\n if ($statusCode === 403) {\n return true;\n }\n\n // Check for scope-related keywords in error message\n $scopeKeywords = ['scope', 'permission', 'forbidden', 'unauthorized', 'access denied', 'oauth'];\n $lowerErrorMessage = strtolower($errorMessage);\n\n foreach ($scopeKeywords as $keyword) {\n if (str_contains($lowerErrorMessage, $keyword)) {\n return true;\n }\n }\n\n // Check response body for scope errors\n if ($responseBody !== null) {\n return $this->arrayContainsKeyword($responseBody, $scopeKeywords);\n }\n\n return false;\n }\n\n /**\n * Recursively search array for keywords in values\n */\n private function arrayContainsKeyword(array $data, array $keywords): bool\n {\n foreach ($data as $value) {\n if (is_array($value)) {\n if ($this->arrayContainsKeyword($value, $keywords)) {\n return true;\n }\n } elseif (is_string($value)) {\n $lowerValue = strtolower($value);\n foreach ($keywords as $keyword) {\n if (str_contains($lowerValue, $keyword)) {\n return true;\n }\n }\n }\n }\n\n return false;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.47672874,"top":0.123703115,"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.48537233,"top":0.123703115,"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.49634308,"top":0.123703115,"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.5049867,"top":0.123703115,"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.51363033,"top":0.123703115,"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.52460104,"top":0.123703115,"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.5355718,"top":0.123703115,"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.5621675,"top":0.123703115,"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.5731383,"top":0.123703115,"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.64261967,"top":0.123703115,"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":"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}]...
|
7945821021870316922
|
-537191020546090905
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
1
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'ac...
|
88386
|
NULL
|
NULL
|
NULL
|
|
88386
|
3013
|
52
|
2026-05-28T17:00:52.994596+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987652994_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-lpr rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-lproidetHubspotClientinterface.ph© HubspotTokenManager.pt© PayloadBuilder.phpoKimol CimosewhooResponseNormalize.phoCSeMCPono© SyncFieldAction.ohoexisiinastaad10.02.23 Vasilev© synckelateoncuvnymanas 24.01.25 Papazov© WebhookSvncBatchProce17.03 25 ianlisteners> MetadataaMicrationP oedriveEh SalesforceafeldsaOpoortunityVatchenOpportunitySyncStrategyProsoec SaarchStrateosametiteê crant nhoC DecorateActivity.php( DeleteObiactsTrait.phpoewanarinithooe nha© PayloadBuilder.phpc) Profile.php© QueryBuilder.php© QueryHandler.php© Queryiterator.php© QueryResults.php© Service.php© SyncBatchRedisService.ptin TraitsRaseeentonoCrmActivityProviderinteorateCCnlACMiWMoh©rmobiectctesower.onC. DefaultProsoectSearchStrateC mallteloer.ond3sindeProscectinternce.onoC) LavouMansoe ono16.0425 wanok710552.0418204.18Graham19.03.18 Grahan2.04.18Olanidi20.10.21 Grahan710262.04.18Grahan2.10.252.10.258.11.182.04.18Graham19.03.18 Grahan4.05.264.05.264.05.26405.262.10.297-1025A05.28A05.28A05.28C Opportun tvActvitwlatcheeeennortur wewn.Ctestomedrnenont tschd nhrHe OrnenontCostrhSrond nhnWindowServiceTestTO0У L7Thu 28 May 20:00:5%+0.Clostoeas noseiveioe40$«11[PHONE]38440442446448sraveloe© RecordSelector.phgC) ACUVIY.or.pC) Team.phd# HS local [liminny@localhostA console (EU) x iii users (EU)console (STAGINGclass Service extends BaseService implements01 A7 A149 V1V33 /1 A v 170%public function importStages(?array Stypes = null, ?string SnissingStageNane = null): ?Stage1706— 1708tean_idnamel=> Sthis->tean->id,3>nhstrirwidthSol"abelestart: 8,wiohSen171612111"type"=> BusinessPnocess: :TYPE_OPPORTUNITY,=> Sp['active'),1713A record type is really a clone of the business process, used to store which record use-/eCreate/update record type cloneSthis->config->recordTypes@->update0rCreatel'cre provider 1d' => Sp('id')— 1715-1728= 1726'tean id= Scns->cean->10,=> mb strinwidth(Sp('label').=> Sp('active')"business process_id' => SbusinessProcess->id ?? nullwidth: 158)172:= schis->cont 10=>scageslo->withTrashedOsnhene("type" Staoc:-TYPE OpPORTUNITy)->0601kewswleeonorowidero=172≤1727=1729=173017391E17321733oneach astaops.asheasradeSs= ResponseNonnalize::normaLizeleal Staae(SdealStace),=1735/** @vac ?Stage SexistingStage *,SeyistainoStaopSoyiistainaStaaesosaet/Sch.d// Restore soft-deleted stages that are now active in HubSpot1f (SexistingStage?->trashed() && $s['active']) (SeyistnoStanp.sractoneoE17571738= 17551703=174=1742= 1143/ Upsert stage (updates soft-deleted records without restoring thenSstage = Sthis->confiq->stages@->withTrashed@->update0rCreate"crnprovider id' => Ss(id'.= 1743—1/40—Cahtedwheda= mb_strinwidth(Ss("label'= mb_strinwidth(Ssf"label'MAN 1041Tx: AutovSo liminnyvBROER PYTnane, M.emare031 A9 A29 V3 /109 A VSELECT * FRON tEaNS WHERE name LIKE "Stounlanes: # 187, 289, 8158SEEiNTCONCAT(U.1d, CASE WHEN U.10 = t.ouner_1d THEN" (owner)" ELSEMrenasisa.*t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.soclable_idJOIN teans t 1.nc->1: on t.id = u.team_1cWHERE u.tean_id = 187 and sa.provider = 'salesfonce':select * fron activities where id = 31264367select * fron contacts where id = 6331639:seleekx tron account anchs 0-450054select * fron opportunities where id = 4843610:#Uodare#'stage_id' = 132730c0un8-4300-4"contact_id' ="updated at" = 2826-95-22 07:16:select * trot1text relavs where created at > *2026-95-01°select * fron actviales order oy 1o desciselect * fron users where nane oike SubrasaSELECT * FROM opportunities WHERE wnid to_bin('04a9cfad-2c87-4453-$|select * fron teans where 1d= 555%select * fron stages where tean_1d = 555:SETSTMTCONCAT(U.Ld, CASE WHEN U.1d = t.ouner_id THEN" (owner)" ELSEhonnen siil coesasccounteeJOIN users u on u.id = sa.sociable.igahittinnetneosonettsWHERE u.tean_id = 100 and sa.provider = 'hubspot":select id, is_closed, is_won, stage_updated.at, crm_provider_id, stclose date, forecast_category, deleted_at, created_at, remotely_crtfron opportunities where tean id = SSS and stage_id = 28616 orderselect id. con_provider id, name.probabilitv, undated at, deletesfron stageswhere id = 28616Cascadeles Orcnnworeeoeionhomwwtino ooooDatelr Luk Fe0 27 16:87318 2826 -020831t. ComDlowor retum deletedisoor© aft choy 8714617963 -100/Carufcec/Con/Hubsnot/Sarusce.ohdDate:Nhu bếy 5 122215k0 282: 1820vSync Hubspot Active Dealsshdex aittedea/ Stvse/T bepot/Servie, php b/spg/Services/Crm/hubspot/Servie.phpNow lct me look at the Nov 2025 buix import = what inportStages looked like at that timo• git log-oneline -beforea*2025-11-19" -after"2025-09-01" -- app/Services/Crm/Hubspot/Service.php | head -S• git shơw 874C3cea56 - app/Services/Crm/Hubspot/Service-php | grep -A 48 "inportStages• oft show 5f6720fd5bsano/Saryices/Crm/Hubspot /Service,oho I areo -A 59 "function StroortStages"public function inportStages(array Stypes & null, ?string SaissingStagelane • null): 75taggSmissingStage = nullspipe lineskesponse # sthis-setient-sgecinsteceuoooecotenierroudecnooothiesCommand ait. areo, tal0 git show 5fd720fdSb:app/Services/Crm/Hubspot/Service-php | grep -A 70 "function importStages" | tail -25Ask anything (XOL)tylwiew oulteonaet today RayKwweunameRun st= (Skip• Ot4 spad...
|
NULL
|
-6859300654772001586
|
NULL
|
click
|
ocr
|
NULL
|
rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-lpr rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-lproidetHubspotClientinterface.ph© HubspotTokenManager.pt© PayloadBuilder.phpoKimol CimosewhooResponseNormalize.phoCSeMCPono© SyncFieldAction.ohoexisiinastaad10.02.23 Vasilev© synckelateoncuvnymanas 24.01.25 Papazov© WebhookSvncBatchProce17.03 25 ianlisteners> MetadataaMicrationP oedriveEh SalesforceafeldsaOpoortunityVatchenOpportunitySyncStrategyProsoec SaarchStrateosametiteê crant nhoC DecorateActivity.php( DeleteObiactsTrait.phpoewanarinithooe nha© PayloadBuilder.phpc) Profile.php© QueryBuilder.php© QueryHandler.php© Queryiterator.php© QueryResults.php© Service.php© SyncBatchRedisService.ptin TraitsRaseeentonoCrmActivityProviderinteorateCCnlACMiWMoh©rmobiectctesower.onC. DefaultProsoectSearchStrateC mallteloer.ond3sindeProscectinternce.onoC) LavouMansoe ono16.0425 wanok710552.0418204.18Graham19.03.18 Grahan2.04.18Olanidi20.10.21 Grahan710262.04.18Grahan2.10.252.10.258.11.182.04.18Graham19.03.18 Grahan4.05.264.05.264.05.26405.262.10.297-1025A05.28A05.28A05.28C Opportun tvActvitwlatcheeeennortur wewn.Ctestomedrnenont tschd nhrHe OrnenontCostrhSrond nhnWindowServiceTestTO0У L7Thu 28 May 20:00:5%+0.Clostoeas noseiveioe40$«11[PHONE]38440442446448sraveloe© RecordSelector.phgC) ACUVIY.or.pC) Team.phd# HS local [liminny@localhostA console (EU) x iii users (EU)console (STAGINGclass Service extends BaseService implements01 A7 A149 V1V33 /1 A v 170%public function importStages(?array Stypes = null, ?string SnissingStageNane = null): ?Stage1706— 1708tean_idnamel=> Sthis->tean->id,3>nhstrirwidthSol"abelestart: 8,wiohSen171612111"type"=> BusinessPnocess: :TYPE_OPPORTUNITY,=> Sp['active'),1713A record type is really a clone of the business process, used to store which record use-/eCreate/update record type cloneSthis->config->recordTypes@->update0rCreatel'cre provider 1d' => Sp('id')— 1715-1728= 1726'tean id= Scns->cean->10,=> mb strinwidth(Sp('label').=> Sp('active')"business process_id' => SbusinessProcess->id ?? nullwidth: 158)172:= schis->cont 10=>scageslo->withTrashedOsnhene("type" Staoc:-TYPE OpPORTUNITy)->0601kewswleeonorowidero=172≤1727=1729=173017391E17321733oneach astaops.asheasradeSs= ResponseNonnalize::normaLizeleal Staae(SdealStace),=1735/** @vac ?Stage SexistingStage *,SeyistainoStaopSoyiistainaStaaesosaet/Sch.d// Restore soft-deleted stages that are now active in HubSpot1f (SexistingStage?->trashed() && $s['active']) (SeyistnoStanp.sractoneoE17571738= 17551703=174=1742= 1143/ Upsert stage (updates soft-deleted records without restoring thenSstage = Sthis->confiq->stages@->withTrashed@->update0rCreate"crnprovider id' => Ss(id'.= 1743—1/40—Cahtedwheda= mb_strinwidth(Ss("label'= mb_strinwidth(Ssf"label'MAN 1041Tx: AutovSo liminnyvBROER PYTnane, M.emare031 A9 A29 V3 /109 A VSELECT * FRON tEaNS WHERE name LIKE "Stounlanes: # 187, 289, 8158SEEiNTCONCAT(U.1d, CASE WHEN U.10 = t.ouner_1d THEN" (owner)" ELSEMrenasisa.*t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.soclable_idJOIN teans t 1.nc->1: on t.id = u.team_1cWHERE u.tean_id = 187 and sa.provider = 'salesfonce':select * fron activities where id = 31264367select * fron contacts where id = 6331639:seleekx tron account anchs 0-450054select * fron opportunities where id = 4843610:#Uodare#'stage_id' = 132730c0un8-4300-4"contact_id' ="updated at" = 2826-95-22 07:16:select * trot1text relavs where created at > *2026-95-01°select * fron actviales order oy 1o desciselect * fron users where nane oike SubrasaSELECT * FROM opportunities WHERE wnid to_bin('04a9cfad-2c87-4453-$|select * fron teans where 1d= 555%select * fron stages where tean_1d = 555:SETSTMTCONCAT(U.Ld, CASE WHEN U.1d = t.ouner_id THEN" (owner)" ELSEhonnen siil coesasccounteeJOIN users u on u.id = sa.sociable.igahittinnetneosonettsWHERE u.tean_id = 100 and sa.provider = 'hubspot":select id, is_closed, is_won, stage_updated.at, crm_provider_id, stclose date, forecast_category, deleted_at, created_at, remotely_crtfron opportunities where tean id = SSS and stage_id = 28616 orderselect id. con_provider id, name.probabilitv, undated at, deletesfron stageswhere id = 28616Cascadeles Orcnnworeeoeionhomwwtino ooooDatelr Luk Fe0 27 16:87318 2826 -020831t. ComDlowor retum deletedisoor© aft choy 8714617963 -100/Carufcec/Con/Hubsnot/Sarusce.ohdDate:Nhu bếy 5 122215k0 282: 1820vSync Hubspot Active Dealsshdex aittedea/ Stvse/T bepot/Servie, php b/spg/Services/Crm/hubspot/Servie.phpNow lct me look at the Nov 2025 buix import = what inportStages looked like at that timo• git log-oneline -beforea*2025-11-19" -after"2025-09-01" -- app/Services/Crm/Hubspot/Service.php | head -S• git shơw 874C3cea56 - app/Services/Crm/Hubspot/Service-php | grep -A 48 "inportStages• oft show 5f6720fd5bsano/Saryices/Crm/Hubspot /Service,oho I areo -A 59 "function StroortStages"public function inportStages(array Stypes & null, ?string SaissingStagelane • null): 75taggSmissingStage = nullspipe lineskesponse # sthis-setient-sgecinsteceuoooecotenierroudecnooothiesCommand ait. areo, tal0 git show 5fd720fdSb:app/Services/Crm/Hubspot/Service-php | grep -A 70 "function importStages" | tail -25Ask anything (XOL)tylwiew oulteonaet today RayKwweunameRun st= (Skip• Ot4 spad...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
88385
|
3012
|
39
|
2026-05-28T17:00:53.095093+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987653095_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
HomeDMsActivityFilesLater Project: faVsco.js, menu
HomeDMsActivityFilesLater..•More+Slack> 0(ah]FileEditViewGoHistoryJiminny ...* Starredplatform-backend-...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# donut_time# engineering# general# happy_birthday& infosec_internal_all# infra-changes# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...WindowHelpSearch: in:#platform-inner-teamX*à platform-inner-teamMessagesBookmarks0 Channel Overview7 RefinementsO Files< PinsP Retro Action ItemsChanges:Monday, May 4th ~• Do not requeue duplicatesjiminny/app May 4th Added by GitHubNikolay Ivanov 1:48 PMhttps://github.com/jiminny/app/pull/12041, фикс за импорта на стейджове (само при hubspot се оказа )но има още една грешка за тях, слагаме стейджове от една организация на другаи не мога да намеря никьде в кода защона еи има две организации които не могат да се изтрият заради товаVasil Vasilev 2:07 PMимаше проблем със мачването на стейджове от една организация на другазаради кеширане на pipelineldпреди горе долу 10тина дена го гледахмеNikolay Ivanov 2:07 PMтой още го иматози пьт при stageвиж ми PRVasil Vasilev 2:08 PMpipelineld бeшe default за едно 5-6 организации, и там се омазваше кешапонеже няма нито crm coniguration id, нито team id включено в кеша дето заминава в РЕДИСа, да, това е сьщотодобре де, аз защо си мисля, че фикс за тоя проблем вече замина на продNikolay Ivanov 2:10 PMMessage & platform-inner-team+100% <78 • Thu 28 May 20:00:528 10Untitled +...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
88384
|
3013
|
51
|
2026-05-28T17:00:44.082908+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987644082_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
1
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'ac...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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":"Show Replace Field","depth":4,"bounds":{"left":0.11868351,"top":0.15482841,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.13131648,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"existingStages","depth":4,"bounds":{"left":0.14228724,"top":0.15403032,"width":0.043882977,"height":0.015961692},"on_screen":true,"value":"existingStages","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.19514628,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.20511968,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.2137633,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.22240691,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/2","depth":4,"bounds":{"left":0.23603724,"top":0.15323225,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.26163563,"top":0.15243416,"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":"Next Occurrence","depth":4,"bounds":{"left":0.27027926,"top":0.15243416,"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":"Filter Search Results","depth":4,"bounds":{"left":0.2789229,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.28756648,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.46210107,"top":0.15243416,"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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.39727393,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.40658244,"top":0.18355946,"width":0.0076462766,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"149","depth":4,"bounds":{"left":0.4162234,"top":0.18355946,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.43018618,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.43949467,"top":0.18355946,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.45179522,"top":0.18355946,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.46077126,"top":0.1819633,"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.4680851,"top":0.1819633,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse HubSpot\\Client\\Crm\\Owners\\Model\\PublicOwner;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->handleOwnersApiException($e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n $profile = $this->processOwner($owner, $teamRepository, $profileRepository);\n\n if ($profile && $userToSearch && $userToSearch->getId() === $profile->getUserId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function handleOwnersApiException(\\HubSpot\\Client\\Crm\\Owners\\ApiException $e): never\n {\n $statusCode = $e->getCode();\n $errorMessage = $e->getMessage();\n $responseBody = $this->parseResponseBody($e->getResponseBody());\n\n $isPermissionError = $this->isPermissionError($statusCode, $errorMessage, $responseBody);\n\n $logContext = [\n 'team_id' => $this->team->getId(),\n 'team_uuid' => $this->team->getUuid(),\n 'config_id' => $this->config->getId(),\n 'status_code' => $statusCode,\n 'error_message' => $errorMessage,\n 'response_body' => $responseBody,\n ];\n\n if ($isPermissionError) {\n $this->logPermissionError($logContext);\n } else {\n $this->logger->error('[HubSpot] Could not sync the profiles.', $logContext);\n }\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n private function parseResponseBody(?string $rawBody): ?array\n {\n if ($rawBody === null || $rawBody === '') {\n return null;\n }\n\n try {\n return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n } catch (\\JsonException) {\n return null;\n }\n }\n\n private function logPermissionError(array $logContext): void\n {\n $this->logger->critical(\n '[HubSpot] ⚠️ PERMISSION ERROR: Cannot sync profiles - Missing OAuth scopes',\n array_merge($logContext, [\n 'action_required' => 'Request additional HubSpot OAuth scopes',\n 'required_scope' => 'crm.objects.owners.read',\n 'impact' => 'Opportunities will have owner_id but NO user_id - AI automation and reporting will fail',\n 'resolution' => 'Manually request scope from HubSpot account admin or re-authenticate',\n ])\n );\n }\n\n private function processOwner(\n PublicOwner $owner,\n TeamRepository $teamRepository,\n ProfileRepository $profileRepository\n ): ?Profile {\n if ($owner->getArchived()) {\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n return null;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n return null;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n if (! $user instanceof User) {\n return null;\n }\n\n return $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n }\n\n private function isPermissionError(int $statusCode, string $errorMessage, ?array $responseBody): bool\n {\n // Check HTTP 403 Forbidden\n if ($statusCode === 403) {\n return true;\n }\n\n // Check for scope-related keywords in error message\n $scopeKeywords = ['scope', 'permission', 'forbidden', 'unauthorized', 'access denied', 'oauth'];\n $lowerErrorMessage = strtolower($errorMessage);\n\n foreach ($scopeKeywords as $keyword) {\n if (str_contains($lowerErrorMessage, $keyword)) {\n return true;\n }\n }\n\n // Check response body for scope errors\n if ($responseBody !== null) {\n return $this->arrayContainsKeyword($responseBody, $scopeKeywords);\n }\n\n return false;\n }\n\n /**\n * Recursively search array for keywords in values\n */\n private function arrayContainsKeyword(array $data, array $keywords): bool\n {\n foreach ($data as $value) {\n if (is_array($value)) {\n if ($this->arrayContainsKeyword($value, $keywords)) {\n return true;\n }\n } elseif (is_string($value)) {\n $lowerValue = strtolower($value);\n foreach ($keywords as $keyword) {\n if (str_contains($lowerValue, $keyword)) {\n return true;\n }\n }\n }\n }\n\n return false;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse HubSpot\\Client\\Crm\\Owners\\Model\\PublicOwner;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->handleOwnersApiException($e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n $profile = $this->processOwner($owner, $teamRepository, $profileRepository);\n\n if ($profile && $userToSearch && $userToSearch->getId() === $profile->getUserId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function handleOwnersApiException(\\HubSpot\\Client\\Crm\\Owners\\ApiException $e): never\n {\n $statusCode = $e->getCode();\n $errorMessage = $e->getMessage();\n $responseBody = $this->parseResponseBody($e->getResponseBody());\n\n $isPermissionError = $this->isPermissionError($statusCode, $errorMessage, $responseBody);\n\n $logContext = [\n 'team_id' => $this->team->getId(),\n 'team_uuid' => $this->team->getUuid(),\n 'config_id' => $this->config->getId(),\n 'status_code' => $statusCode,\n 'error_message' => $errorMessage,\n 'response_body' => $responseBody,\n ];\n\n if ($isPermissionError) {\n $this->logPermissionError($logContext);\n } else {\n $this->logger->error('[HubSpot] Could not sync the profiles.', $logContext);\n }\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n private function parseResponseBody(?string $rawBody): ?array\n {\n if ($rawBody === null || $rawBody === '') {\n return null;\n }\n\n try {\n return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n } catch (\\JsonException) {\n return null;\n }\n }\n\n private function logPermissionError(array $logContext): void\n {\n $this->logger->critical(\n '[HubSpot] ⚠️ PERMISSION ERROR: Cannot sync profiles - Missing OAuth scopes',\n array_merge($logContext, [\n 'action_required' => 'Request additional HubSpot OAuth scopes',\n 'required_scope' => 'crm.objects.owners.read',\n 'impact' => 'Opportunities will have owner_id but NO user_id - AI automation and reporting will fail',\n 'resolution' => 'Manually request scope from HubSpot account admin or re-authenticate',\n ])\n );\n }\n\n private function processOwner(\n PublicOwner $owner,\n TeamRepository $teamRepository,\n ProfileRepository $profileRepository\n ): ?Profile {\n if ($owner->getArchived()) {\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n return null;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n return null;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n if (! $user instanceof User) {\n return null;\n }\n\n return $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n }\n\n private function isPermissionError(int $statusCode, string $errorMessage, ?array $responseBody): bool\n {\n // Check HTTP 403 Forbidden\n if ($statusCode === 403) {\n return true;\n }\n\n // Check for scope-related keywords in error message\n $scopeKeywords = ['scope', 'permission', 'forbidden', 'unauthorized', 'access denied', 'oauth'];\n $lowerErrorMessage = strtolower($errorMessage);\n\n foreach ($scopeKeywords as $keyword) {\n if (str_contains($lowerErrorMessage, $keyword)) {\n return true;\n }\n }\n\n // Check response body for scope errors\n if ($responseBody !== null) {\n return $this->arrayContainsKeyword($responseBody, $scopeKeywords);\n }\n\n return false;\n }\n\n /**\n * Recursively search array for keywords in values\n */\n private function arrayContainsKeyword(array $data, array $keywords): bool\n {\n foreach ($data as $value) {\n if (is_array($value)) {\n if ($this->arrayContainsKeyword($value, $keywords)) {\n return true;\n }\n } elseif (is_string($value)) {\n $lowerValue = strtolower($value);\n foreach ($keywords as $keyword) {\n if (str_contains($lowerValue, $keyword)) {\n return true;\n }\n }\n }\n }\n\n return false;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.47672874,"top":0.123703115,"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.48537233,"top":0.123703115,"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.49634308,"top":0.123703115,"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.5049867,"top":0.123703115,"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.51363033,"top":0.123703115,"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.52460104,"top":0.123703115,"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.5355718,"top":0.123703115,"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.5621675,"top":0.123703115,"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.5731383,"top":0.123703115,"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.64261967,"top":0.123703115,"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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"31","depth":4,"bounds":{"left":0.60039896,"top":0.14844373,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.61203456,"top":0.14844373,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"29","depth":4,"bounds":{"left":0.62200797,"top":0.14844373,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.6343085,"top":0.14844373,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"109","depth":4,"bounds":{"left":0.6442819,"top":0.14844373,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.65791225,"top":0.14684756,"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.66522604,"top":0.14684756,"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":"SELECT * FROM team_features where team_id = 1;\n\nSELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922\nSELECT * FROM users WHERE team_id = 340; # 12015\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 340\nand sa.provider = 'salesforce';\n# and sa.provider = 'salesloft';\n\nselect * from crm_fields where crm_configuration_id = 270 and object_type = 'event';\n# 125558 - Event Type - Event_Type__c\n# 125552 - Event Status - Event_Status__c\n\nSELECT * FROM sidekick_settings WHERE team_id = 340;\n\nSELECT * FROM crm_field_values WHERE crm_field_id in (125552);\n\nselect * from activities where crm_configuration_id = 270\nand type = 'conference' and crm_provider_id IS NOT NULL\nand actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;\n\nSELECT * FROM activities WHERE id = 20871677;\nSELECT * FROM crm_field_data WHERE activity_id = 20871677;\n\nselect * from crm_layouts where crm_configuration_id = 270;\nselect * from crm_layout_entities where crm_layout_id in (886,887);\n\nSELECT * FROM crm_configurations WHERE id = 270;\n\nselect * from playbooks where team_id = 340; # 1514\nselect * from groups where team_id = 340;\nSELECT * FROM crm_fields WHERE id IN (125393, 125401);\n\nselect g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g\njoin playbooks p on g.playbook_id = p.id\njoin crm_fields f on p.activity_field_id = f.id\nwhere g.team_id = 340;\n\nSELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716\nselect * from crm_field_data where object_id = 20448716;\n\nselect * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008\nselect * from opportunities where team_id = 343;\nselect * from opportunities where team_id = 343 and crm_provider_id = '18099102526';\nselect * from opportunities where team_id = 343 and account_id = 945217482;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from accounts where team_id = 343 order by name asc;\n\nselect * from stages where crm_configuration_id = 273 and type = 'opportunity';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143\nSELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;\nSELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';\nSELECT * FROM activities WHERE id = 20717903;\n\nselect * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 353\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, l.atkinson@mwbsolutions.co.uk\nSELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;\n# id: 20940638, user: 12022, contact: 5305871\nSELECT * FROM activity_summary_logs WHERE activity_id = 20940638;\nselect * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 345\nand sa.provider = 'hubspot';\n\nselect * from users where team_id = 345 and id = 12022;\nSELECT * FROM crm_profiles WHERE user_id = 12022;\nSELECT * FROM participants WHERE activity_id = 20940638;\nSELECT * FROM users u\nJOIN crm_profiles cp ON u.id = cp.user_id\nWHERE u.team_id = 345;\n\nselect * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871\n\nselect * from team_features where team_id = 345;\nSELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197\nSELECT * FROM participants WHERE activity_id = 20897406;\n\n\n\nSELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912\nSELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';\n\n\nSELECT * FROM activities WHERE id = 20946641;\nSELECT * FROM crm_profiles WHERE user_id = 10211;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, triger@lunio.ai\nSELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';\nselect * from stages where crm_configuration_id = 97 and type = 'opportunity';\nselect * from opportunities where team_id = 120;\n\n\nselect * from crm_configurations crm join teams t on crm.id = t.crm_id\nwhere 1=1\nAND t.current_billing_plan IS NOT NULL\nAND crm.auto_sync_activity = 0\nand crm.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,james.lewendon@exclaimer.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 270\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956\nSELECT * FROM crm_profiles WHERE user_id = 11446;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, alex.chikly@cygnetise.com\nselect * from playbooks where team_id = 372;\nselect * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340\nSELECT * FROM crm_field_values WHERE crm_field_id = 141340;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 372\nand sa.provider = 'salesforce';\n\nselect * from crm_profiles where crm_configuration_id = 300;\nSELECT * FROM crm_configurations WHERE team_id = 372;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,mfa@planday.com\nSELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756\nselect * from crm_field_data where object_id = 3207756;\nSELECT * FROM crm_fields WHERE id = 111834;\n\nselect f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value\nFROM crm_fields f\nJOIN crm_field_data fd ON f.id = fd.crm_field_id\nWHERE f.crm_configuration_id = 242\nAND f.object_type = 'opportunity'\nAND fd.object_id IN (3207756)\nORDER BY fd.object_id, fd.updated_at;\n\nSELECT * FROM crm_configurations WHERE auto_connect = 1;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,salesforce-admin@tourlane.com\nselect * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id\nwhere g.team_id = 187;\n\nselect * from `groups` where team_id = 187;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 187\nand sa.provider = 'salesforce';\n\n# Destination - 98870 - Destination__c\n# Stage - 79014 - StageName\n# Land Arrangement - 98856 - Land_Arrangement__c\n# Flight - 98848 - Flight__c\n# Last activity date - 98812 - LastActivityDate\n# Last modified date - 98809 - LastModifiedDate\n# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c\n# next call - 98864 - Next_Call__c\n\nselect * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\nselect * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';\nselect * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;\nselect * from activities where opportunity_id = 3538248;\n\nSELECT * FROM crm_profiles WHERE user_id = 8150;\n\nselect * from deal_risks where opportunity_id = 3538248;\n\nselect * from teams where crm_id IS NULL;\n\nSELECT opp.id AS opportunity_id,\n u.group_id AS group_id,\n MAX(\n CASE\n WHEN a.type IN (\"sms-inbound\", \"sms-outbound\") THEN a.created_at\n ELSE a.actual_end_time\n END) as last_date\nFROM opportunities opp\nleft join activities a on a.opportunity_id = opp.id\ninner join users u on opp.user_id = u.id\nwhere opp.user_id IN (9951)\n\nAND opp.is_closed = 0\nand a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL\ngroup by opp.id;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,polly.morphew@cybsafe.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 301;\nSELECT * FROM contacts WHERE id = 6612363;\nSELECT * FROM accounts WHERE id = 4235676;\nSELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;\nselect * from opportunity_stages where opportunity_id = 4503759;\n# SELECT * FROM opportunities WHERE id = 4569937;\n\nselect * from activities where crm_configuration_id = 301;\nSELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370\nSELECT * FROM participants WHERE activity_id = 26330370;\n\nSELECT * FROM teams WHERE id = 375;\nselect * from playbooks where team_id = 375;\n\nselect * from stages where crm_configuration_id = 301 and type = 'opportunity';\n\nselect * from teams;\nselect * from contact_roles;\n\nSELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';\n\nselect * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;\n\nSELECT * FROM crm_field_data WHERE object_id = 3771706;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'\nand crm_provider_id LIKE \"%traffic_light%\";\nSELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);\n\nSELECT fd.* FROM opportunities o\nJOIN crm_field_data fd ON o.id = fd.object_id\nWHERE o.team_id = 343\n# and o.user_id IS NOT NULL\nand fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)\nand fd.value != ''\norder by value desc\n# group by o.id\n;\n\nSELECT * FROM opportunities WHERE id = 3769843;\n\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, salesforce-admin@tourlane.com\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,aswini.mishra@fundingcircle.com\nSELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839\n\n\nSELECT * FROM opportunities WHERE id = 3855992;\n\nSELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988\n\nSELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';\n\nselect * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507\nSELECT * FROM crm_field_data WHERE object_id = 5874411;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379\nand sa.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, nikhil.kumar@mention-me.com\nSELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\nSELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793\nselect * from generic_ai_prompts where subject_id = 3537793;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, triger@lunio.ai\nSELECT * FROM crm_configurations WHERE id = 97;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 97;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;\nSELECT * FROM crm_fields WHERE id = 32682;\n\nselect cfd.value, o.* from opportunities o\njoin crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682\nwhere team_id = 120\nand cfd.value != ''\n;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 120\nand sa.provider = 'salesforce';\n\nselect * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';\nSELECT * FROM crm_field_data WHERE object_id = 2313439;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 410;\nSELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';\nselect * from scorecards where team_id = 410;\nselect * from scorecard_rules;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, aswini.mishra@fundingcircle.com\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\njoin users u on o.user_id = u.id\nwhere a.crm_configuration_id = 177 and a.type LIKE '%email-out%'\n# and a.actual_end_time > '2024-12-16 00:00:00'\n# and o.remotely_created_at > '2024-12-01 00:00:00'\n# and u.group_id = 1014\nand u.id = 9021\norder by a.id desc;\nSELECT * FROM opportunities WHERE id in (3981384,4017346);\nSELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);\n\nselect * from users where id = 9021;\nselect * from inboxes where user_id = 9021;\n\nselect * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';\n\nselect * from email_messages where team_id = 220\nand orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'\nand subject LIKE '%Personal%'\n# and 'from' = 'credit@fundingcircle.com'\n;\n\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\nwhere a.user_id = 9021 and a.type LIKE '%email-out%'\nand a.actual_end_time > '2024-12-18 00:00:00'\nand o.user_id IS NOT NULL\nand o.remotely_created_at > '2024-12-01 00:00:00'\norder by a.id desc;\n\nSELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;\nselect * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;\n\nselect * from team_settings where name IN ('useCloseDate');\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, jfarrell@hurree.co\nSELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 104\nand sa.provider = 'hubspot';\n\nselect * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'\nselect * from teams where crm_id IS NULL;\n\nselect t.name as 'team', u.name as 'owner', u.email, u.phone\nfrom teams t\njoin activity_providers ap on t.id = ap.team_id\njoin users u on t.owner_id = u.id\nwhere 1=1\n and t.status = 'active'\n and ap.is_enabled = 1\n# and u.status = 1\n and ap.provider = 'ms-teams';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nSELECT * FROM teams WHERE id = 442; # 14293\nselect * from users where team_id = 442;\nselect * from social_accounts sa where sa.sociable_id = 14293;\nselect * from invitations where team_id = 442;\n\n# ********************************************************************************************************\nSELECT * FROM users WHERE email LIKE '%nea.liikamaa@eletive.com%'; # 14022\nSELECT * FROM teams WHERE id = 429;\nselect * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);\nselect * from activities where opportunity_id in (4340436,4353519);\n\nselect * from transcription where activity_id IN (25630961,25381771);\nselect * from generic_ai_prompts where subject_id IN (4353519);\n\nSELECT\n a.id as activity_id,\n a.opportunity_id,\n a.type as activity_type,\n a.language,\n CONCAT(a.title, a.description) AS mail_content,\n e.from AS mail_from,\n e.to AS mail_to,\n e.subject AS mail_subject,\n e.body AS mail_body,\n p.type as prompt_type,\n p.status as prompt_status,\n p.content AS prompt_content,\n a.actual_start_time as created_at\nFROM activities a\n LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL\n LEFT JOIN email_messages e ON a.id = e.activity_id\nWHERE a.actual_start_time > '2024-01-01 00:00:00'\n AND a.opportunity_id IN (4353519)\n AND a.status IN ('completed', 'received', 'delivered')\n AND a.deleted_at IS NULL\n AND a.type NOT IN ('sms-inbound', 'sms-outbound')\nORDER BY a.opportunity_id ASC, a.id ASC;\n\nSELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293\nSELECT * FROM teams WHERE id = 442;\nSELECT * FROM crm_configurations WHERE id = 344;\nselect * from team_features where team_id = 442;\nselect * from groups where team_id = 442;\nselect * from playbooks where team_id = 442;\nselect * from playbook_categories where playbook_id = 1729;\nselect * from crm_fields where crm_configuration_id = 344 and id = 172024;\nSELECT * FROM crm_field_values WHERE crm_field_id = 172024;\nselect * from crm_layouts where crm_configuration_id = 344;\nselect * from playbook_layouts where playbook_id = 1729;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444\n\nselect s.*\n# , s.sent_at, u.name, a.*\nfrom activity_summary_logs s\ninner join activities a on a.id = s.activity_id\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 356\nand s.sent_at > date_sub(now(), interval 60 day)\norder by a.actual_end_time desc;\n\nselect * from activities a\n# inner join activity_summary_logs s on s.activity_id = a.id\nwhere a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)\n# and a.crm_provider_id is not null\n# and provider <> 'ringcentral'\nand status = 'completed'\norder by a.actual_end_time desc;\n\nselect * from teams order by id desc; # 17328, 32, 17830, integration-account@jiminny.com\nSELECT * FROM users;\nSELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active\nSELECT * FROM teams WHERE id = 260;\nselect * from team_settings where team_id = 260;\nselect * from crm_configurations where team_id = 260;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 356;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;\n\nselect * from accounts where crm_configuration_id = 221 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 221 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 221 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 221 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 221;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 221 order by id desc;\nselect * from stages where crm_configuration_id = 221 order by id desc;\n\nselect * from accounts where crm_configuration_id = 356 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 356 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 356 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 356 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 356;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 356 order by id desc;\nselect * from stages where crm_configuration_id = 356 order by id desc;\n\nselect * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)\nselect * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)\nselect * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4\nselect ce.* from calendars c\njoin users u on c.user_id = u.id\njoin calendar_events ce on c.id = ce.calendar_id\nwhere u.team_id = 260\nand (ce.start_time > '2025-02-21 00:00:00')\n;\n# calendar events 1207\n#\n\nselect * from opportunities where team_id = 260;\nSELECT * FROM crm_field_data WHERE object_id = 4696496;\n\nselect * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;\nselect * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')\n# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0\nand created_at > '2024-03-01 00:00:00'\norder by id desc; # 880 000, ringcentral, avaya\nSELECT * FROM participants WHERE activity_id = 26371744;\n\n# all activities 942 000 +\n# conference 7385 - scheduled 984 - external 343\n\nselect * from activities where id = 26321812;\nselect * from participants where activity_id = 26321812;\nselect * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);\nselect * from leads where id in (720428,689175,731546,645866,621037);\n\nselect * from users where id = 13841;\nselect * from opportunities where user_id = 9541;\nselect * from stages where id = 15900;\n\nselect * from accounts where\n# id IN (4160055,5053725,4965303,4896434)\nid in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)\n;\n\nselect * from activities where id = 26654935;\nSELECT * FROM opportunities WHERE id = 4803458;\n\nSELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;\nSELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time\nFROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);\n\nSELECT DISTINCT\n o.id, o.stage_id, s.name, a.title,\n a.*\nFROM activities a\n# INNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nINNER JOIN groups g ON u.group_id = g.id\nINNER JOIN opportunities o ON a.opportunity_id = o.id\nINNER JOIN stages s ON o.stage_id = s.id\nWHERE\n a.crm_configuration_id = 356\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 13841\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')\n AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')\n\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n )\n )\n AND (\n# s.id = 15900\n s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')\n OR s.uuid IS NULL -- Include records without opportunity stage\n )\n\nORDER BY a.actual_end_time DESC;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, willsc@leadforensics.com\nSELECT * FROM users WHERE team_id = 190;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 190\nand sa.provider = 'hubspot';\n\nselect * from role_user where user_id = 8474;\n\nselect * from crm_configurations where provider = 'bullhorn';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;\nSELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;\n\nSELECT * FROM opportunities WHERE id = 4732493;\nselect * from activities where opportunity_id = 4732493;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 443; # 358, 14315, andrea.romano@correrenaturale.com\nSELECT * FROM opportunities WHERE team_id = 443;\n\nSELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id\nFROM activities AS a\nJOIN stages AS s ON a.stage_id = s.id\nJOIN users AS u ON u.id = a.user_id\nJOIN teams AS t ON t.id = s.team_id\nWHERE u.team_id <> s.team_id and t.id > 135;\n\n\nSELECT\n crm_configuration_id,\n crm_provider_id,\n COUNT(*) as duplicate_count,\n GROUP_CONCAT(id) as stage_ids,\n GROUP_CONCAT(name) as stage_names\nFROM stages\nGROUP BY crm_configuration_id, crm_provider_id\nHAVING COUNT(*) > 1\nORDER BY duplicate_count DESC;\n\nselect * from stages where id IN (14898,14907);\n\nselect * from business_processes;\n\nSELECT *\nFROM crm_configurations\nWHERE team_id IN (\n SELECT team_id\n FROM crm_configurations\n GROUP BY team_id\n HAVING COUNT(*) > 1\n)\nORDER BY team_id;\n\nSELECT *\nFROM teams\nWHERE crm_id IN (\n SELECT crm_id\n FROM teams\n GROUP BY crm_id\n HAVING COUNT(*) > 1\n)\nORDER BY crm_id;\n\n# ***************************************************************************\nselect * from crm_configurations where provider = 'integration-app';\nSELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 andrea.romano@correrenaturale.com\nselect * from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect * from team_features where team_id = 358;\nselect * from activity_summary_logs;\n\nselect * from teams where id = 406;\n\n# ************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, srv.salesforce@sportfive.com\nselect * from activities where crm_configuration_id = 202 order by actual_end_time desc;\n\nSELECT * FROM users where id = 14637;\nSELECT * FROM teams where id = 267;\nSELECT * FROM groups where id = 1118;\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 202\n AND status IN ('completed', 'failed')\n AND recording_state != 'stopped'\n AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n AND (is_private = 0 OR user_id = 14637)\n AND (\n (\n actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n ) OR (\n actual_start_time IS NULL\n AND type IN ('sms-outbound', 'sms-inbound')\n AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tracks\n WHERE\n tracks.activity_id = activities.id\n AND tracks.type IN ('audio', 'video')\n )\nORDER BY actual_end_time DESC;\n\nSELECT DISTINCT\n a.*\nFROM activities a\nINNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nWHERE\n a.crm_configuration_id = 202\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 14637\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND a.user_id = 14637\n )\n )\n\nORDER BY a.actual_end_time DESC\n;\n\nSELECT DISTINCT a.*\nFROM activities a\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams t ON u.team_id = t.id\n# INNER JOIN tracks tr ON a.id = tr.activity_id\n# INNER JOIN groups g ON u.group_id = g.id\nWHERE 1=1\n AND t.id = 267\n# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND tr.type NOT IN ('audio', 'video')\n AND (\n a.is_private = 0\n OR a.user_id = 14637\n )\n AND (\n (a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')\n OR (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'\n )\n )\n# and NOT EXISTS (\n# SELECT 1\n# FROM tracks t\n# WHERE t.activity_id = a.id\n# AND t.type IN ('audio', 'video')\n# )\n\nORDER BY a.actual_end_time DESC;\n\nSELECT * FROM tracks WHERE activity_id = 26485995;\n\nselect a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 202\n# and a.is_internal = 0\nand (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type IN (\"softphone\",\"softphone-inbound\",\"conference\",\"sms-inbound\")\nand a.status IN ('completed', 'failed')\n# and a.external_id is not null\norder by a.actual_end_time desc;\n\nselect * from activities a where a.crm_configuration_id = 202\nand a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'\n# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_field_data WHERE crm_field_id = 98809;\n\nselect * from users where status = 1 AND timezone = 'MDT';\n\nselect * from opportunities where id = 3769814;\nselect * from deal_risks where opportunity_id = 3769814;\n\nselect cp.* from crm_profiles cp\njoin users u on cp.user_id = u.id\njoin crm_configurations crm on cp.crm_configuration_id = crm.id\nwhere crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';\n\nselect * from crm_fields where id = 154575;\n\nselect * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';\nSELECT * FROM teams WHERE id = 176; # crm 148\nselect * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nselect * from crm_fields cf\njoin crm_configurations crm on crm.id = cf.crm_configuration_id\nwhere crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');\n\n# *********************************************************************************************\nSELECT * FROM users WHERE id IN (15415, 15418);\nSELECT * FROM groups WHERE id IN (1805,1806);\nSELECT * FROM playbooks WHERE id = 1860;\nSELECT * FROM playbook_categories WHERE id = 38634;\nSELECT * FROM crm_fields WHERE id = 189962;\n\nSELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 raza.gilani@vuelio.com\n\nSELECT * FROM crm_profiles WHERE user_id = 15415;\nSELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';\n\nselect * from sidekick_settings where team_id = 472;\n\nSELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418\nSELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415\n\n# *********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, salesforce-integrations@teamtailor.com\nselect * from crm_configurations where id = 218;\nSELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765\nSELECT * FROM users WHERE id IN (13232, 13230);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n0057R00000EPL5HQAX Inez Ekblad\n\n1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur\n\nSELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);\n\n############################################################################################\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id IN (94491,94493,94498);\nSELECT * FROM users WHERE id = 13658;\nSELECT * FROM teams WHERE id = 109;\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, katy.holden@strengthscope.comk\nSELECT * FROM stages WHERE crm_configuration_id = 390;\nselect * from business_processes where team_id = 481 and crm_configuration_id = 390;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 481\nand sa.provider = 'salesforce';\n\n\nSELECT * FROM users WHERE id = 15780; # team 462\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 462\nand sa.provider = 'hubspot';\n\n\nselect * from teams where id = 495;\nSELECT * FROM users WHERE id = 15794;\nselect * from social_accounts where sociable_id = 15794;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752\nSELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794\nSELECT * FROM activities WHERE crm_configuration_id = 407\nand status = 'completed' and type = 'conference'\norder by id desc;\n\nselect ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id\njoin permission_role pr on pr.role_id = ru.role_id\n join permissions p on p.id = pr.permission_id\nwhere team_id = 495 and p.name IN ('dial');\n\nselect * from permission_role;\n\nselect * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;\nSELECT * FROM activities WHERE id = 29512773;\nSELECT * FROM activities WHERE id IN (29042721,28991325,29002874);\n\nSELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 407\n# and a.id IN (29042721,28991325,29002874);\n\nSELECT * FROM users WHERE id = 15794;\nSELECT * FROM users WHERE team_id = 495;\nSELECT * FROM social_accounts WHERE sociable_id = 15794;\nSELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';\nSELECT * FROM contacts WHERE team_id = 495;\nSELECT * FROM leads WHERE team_id = 495;\nSELECT * FROM accounts WHERE team_id = 495;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 407;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 407;\nSELECT * FROM crm_configurations WHERE id = 407;\nSELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'\nand user_id IS NOT NULL and is_closed = 1 and is_won = 1;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103\nSELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064\nSELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');\n\n# *********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 325\nand sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085\nSELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733\nSELECT * FROM activity_summary_logs where activity_id = 28719733;\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444\nSELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';\nSELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630\nselect * from activities where crm_configuration_id = 356 and lead_id = 841732;\n\nSELECT * from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 356;\n\nselect * from activities where crm_configuration_id = 356\nand actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'\norder by id desc;\n\nselect * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;\nselect * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\n\nselect * from team_features where team_id = 260;\nselect * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;\n\nselect * from crm_fields;\nselect * from crm_layout_entities;\n\nSELECT * FROM teams WHERE name LIKE '%Optable%';\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id in (94491,94493,94498);\n\nselect * from teams where crm_id IS NULL;\n\nSELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;\n\n# *************************************************************************************************\nselect * from team_domains where team_id = 399;\nSELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207\n\nselect * from calendar_events where id = 5163781;\nSELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896\nSELECT * FROM participants WHERE activity_id = 29443896;\nselect * from contacts where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\nselect * from leads where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\n\nselect * from activities where user_id = 14937 order by created_at ;\n\nselect * from users where id = 14937;\n\nselect * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';\nselect * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';\n\nselect * from activities a join participants p on a.id = p.activity_id\nwhere crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';\n\n# *************************************************************************************************\nSELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';\nSELECT * FROM opportunities WHERE team_id = 379 order by id desc;\nSELECT * FROM teams WHERE id = 379;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379 and sociable_id = 13852\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE id = 307;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 307;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307\n and id IN (144750,144855,145158,155227);\n\nSELECT * FROM activities;\n\n\nselect * from activities\nwhere created_at > '2025-07-01 00:00:00'\n# and created_at < '2025-08-01 00:00:00'\nand type not in ('email-outbound', 'email-inbound')\nand account_id is null\nand contact_id is null\nand lead_id is null\nand opportunity_id is not null\n;\nSELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);\nSELECT * FROM crm_configurations WHERE id in (335,301,200);\n\nselect * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';\n\nSELECT * FROM teams WHERE name LIKE '%Resights%';\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nselect * from teams where id IN (442);\n\nselect * from activities\nwhere crm_configuration_id = 177\nand provider = 'amazon-connect'\n order by id desc;\n# and source <> 'gong';\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nSELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;\n\n\nselect * from crm_configurations where store_transcript = 1;\nSELECT * FROM teams WHERE id IN (80);\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 277\nand sa.provider = 'salesforce';\n\nselect * from activities where crm_configuration_id = 213 and account_id = 2511502;\n\nselect * from crm_configurations where id = 213;\n\nSELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604\nSELECT * FROM participants WHERE activity_id = 33981604;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 431\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223\nselect * from activity_summary_logs where activity_id = 33997223;\nselect * from activity_notes where activity_id = 33997223;\n\n# ***********************************\nSELECT * FROM teams WHERE name LIKE '%Abode%';\n\n\nselect * from features;\nselect * from teams t\nwhere t.status = 'active'\nand id NOT IN (select team_id from team_features where feature_id = 9)\n;\n\n\nselect * from playbook_layouts where playbook_id = 1725;\nSELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473\nselect * from teams where id = 318;\nselect * from crm_configurations where team_id = 318;\nselect * from playbooks where team_id = 318;\nSELECT * FROM crm_layouts where crm_configuration_id = 381;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;\nSELECT * FROM crm_fields WHERE id IN (192938,192936,192939);\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;\nSELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);\n\nSELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533\n\n\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nSELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);\n\nSELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124\nselect * from crm_layouts where crm_configuration_id = 281;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nselect * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);\nselect * from opportunities where crm_configuration_id = 281;\n\nSELECT * FROM activities WHERE id IN (34211315, 34130075);\nSELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);\n\nselect cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd\njoin crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id\njoin crm_fields cf on cle.crm_field_id = cf.id\nwhere cf.deleted_at IS NOT NULL\nGROUP BY cle.id, cf.id;\n\nselect * from crm_layouts where id IN (355);\nselect u.email, t.crm_id, t.* from teams t\njoin users u on u.id = t.owner_id\nwhere crm_id IN (97);\n\nSELECT * FROM crm_fields WHERE id = 96492;\n\nselect * from permissions;\nselect * from permission_role where permission_id = 247;\nselect * from roles;\n\nselect * from migrations;\n# *****************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('291e3c21-11cc-4728-aee7-6e4bedf86d72') = uuid; # 34262174\nSELECT * FROM crm_configurations WHERE id = 301;\nSELECT * FROM teams WHERE id = 343;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from participants where activity_id = 34262174;\n\nselect * from contacts where crm_configuration_id = 301 and id = 6976326;\nselect * from accounts where crm_configuration_id = 301 and id IN (4647626, 4815829); # 30761335403\n\nselect * from activity_summary_logs where activity_id = 34262174;\n\nselect * from users where status = 1 AND timezone = 'EST';\n\n# ****************************************************************************\nSELECT * FROM users WHERE id = 13869;\nSELECT * FROM crm_configurations WHERE id = 320;\nSELECT * FROM teams WHERE id = 401;\n\nSELECT * FROM activities WHERE uuid_to_bin('2228c16f-10be-48d5-90d4-67385219dc01') = uuid; # 29670601\n\nSELECT * FROM accounts WHERE id = 7761483;\nSELECT * FROM opportunities WHERE id = 6051814;\n\nSELECT * FROM teams WHERE name LIKE '%Seedlegals%';\n\n;select * from opportunities where updated_at > '2025-10-11' AND crm_provider_id = '34713761166';\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 177;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 577;\nSELECT * FROM crm_fields WHERE id IN (68458,68459,68480,68497,68524,68530,68554,68618,68662,68781,68810,68898,68981,69049,97467);\n\nSELECT t.id, crm.id, t.name, crm.sync_objects, crm.provider, crm.last_synced_at FROM crm_configurations crm join teams t on t.crm_id = crm.id\nwhere t.status = 'active' AND crm.provider = 'hubspot' AND crm.last_synced_at < '2025-10-22 00:00:00';\n\nSELECT * FROM activities WHERE uuid_to_bin('fa09449f-cba9-496a-b8f3-865cd3c72351') = uuid;\nSELECT * FROM crm_configurations where id = 184;\nSELECT * FROM teams WHERE id = 246;\nSELECT * FROM social_accounts WHERE sociable_id = 9259 and provider = 'hubspot';\n\nSELECT * FROM users WHERE email LIKE '%rhian.old@bud.co.uk%'; # 17700\nSELECT * FROM teams WHERE id = 551;\n\nSELECT * FROM crm_configurations WHERE id = 471;\nSELECT * FROM activities WHERE crm_configuration_id = 471 and crm_provider_id IS NOT NULL;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 471;\nSELECT * FROM crm_fields WHERE id = 307260;\nSELECT * FROM crm_field_values WHERE crm_field_id = 307260;\n\nselect * from crm_layouts where crm_configuration_id = 471;\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1547;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1548;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 551 and sa.provider = 'hubspot';\n\nSELECT * FROM teams WHERE name LIKE '%$PCS%';\n\n# ********************************************************************************************************\nselect * from crm_configurations crm\njoin teams t on t.crm_id = crm.id\nwhere t.status = 'active'\nand crm.provider = 'hubspot';\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 crm_configurations where id = 331; # 416\nSELECT * FROM teams WHERE id = 416;\nSELECT * FROM opportunities WHERE team_id = 190;\n\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%';\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 190 and sa.provider = 'hubspot';\n\n\n\nSELECT * FROM teams WHERE name LIKE '%Rapaport%'; # 431, 337\nSELECT * FROM teams where id = 431;\nSELECT * FROM crm_configurations where team_id = 431;\nSELECT * FROM activity_providers where team_id = 431;\nSELECT * FROM activities where crm_configuration_id = 337 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 431 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%BiP%'; # 401, 320\nSELECT * FROM teams where id = 401;\nSELECT * FROM crm_configurations where team_id = 401;\nSELECT * FROM activity_providers where team_id = 401;\nSELECT * FROM activities where crm_configuration_id = 320 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 401 and sa.provider = 'salesforce';\n\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 307; # 379 - Story Terrace Inc , portalId: 3921157\nSELECT * FROM contacts WHERE team_id = 379 and updated_at > '2026-01-31 11:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 379 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; # 563 - LATUS Group (ad94d501-5d09-44fd-878f-ca3a9f8865c3) , portalId: 3904501\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 338; # 432 - Formalize , portalId: 9214205\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 432 and sa.provider = 'hubspot';\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 436; # 519 - Moxso , portalId: 25531989\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 96; # 119 - Nourish Care , portalId: 26617984\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 331; # 416 - The National College , portalId: 7213852\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 308; # 380 - Foodles , portalId: 7723616\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 379; # 471 - imat-uve , portalId: 9177354\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 465; # 545 - Spotler , portalId: 144759271\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 455; # 537 - indevis , portalId: 25666868\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 200; # 265 - Jobadder , portalId: 6426676\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 335; # 429 - Eletive , portalId: 6110563\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 363; # 456 - Global Group , portalId: 8901981\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 297; # 369 - Unbiased , portalId: 9229005\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 353; # 449 - Fuuse , portalId: 25781745\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 487; # 566 - Nimbus , portalId: 39982590\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 487;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1630;\nselect * from crm_fields where crm_configuration_id = 487 and\n(uuid_to_bin('4c6b2971-64d4-45b8-b377-427be758b5a5') = uuid or uuid_to_bin('59e368d8-65a0-4b77-b611-db37c99fbe68') = uuid);\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 420; # 506 - voiio , portalId: 145629154\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 479; # 558 - Momice , portalId: 535962\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 59; # 80 - Storyclash GmbH , portalId: 4268479\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 175; # 203 - Team iAM , portalId: 5534732\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 368; # 460 - OneTouch Health , portalId: 5534732183355\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\n\n\nselect * from users where id = 29643;\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM teams WHERE name LIKE '%Buynomics%'; # 462, 482, 14910\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\n# and description like '%The call focused on understanding Welch%'\norder by id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 462 and sa.provider = 'salesforce';\n\nselect * from contacts where crm_configuration_id = 482 and name = 'Cyndall Hill'; # 15504749\nselect * from contacts where id = 10891096; # 482\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\nand contact_id = 15504749\norder by id desc;\n\nselect * from activities where id = 36793003; # 96cc7bc1-8622-4d27-92f4-baf664fc1a56, 00UOf00000PDdOXMA1\nselect * from transcription where id = 7646782;\nselect * from ai_prompts where transcription_id = 7646782;\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7a8471a3-847e-4822-802b-ddf426bbc252') = uuid; # 37370018\nSELECT * FROM activity_summary_logs WHERE activity_id = 37370018;\nSELECT * FROM teams WHERE id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 555 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7c17b8aa-09df-4f85-a0f7-51f47afd712d') = uuid; # 37395250\nSELECT * FROM activities WHERE uuid_to_bin('14d60388-260d-494b-aa0d-63fdb1c78026') = uuid; # 37395250\n\nSELECT a.* FROM activities a JOIN crm_configurations c on c.id = a.crm_configuration_id\nwhere a.type IN ('softphone', 'softphone-outbound') and c.provider = 'hubspot'\nand a.provider NOT IN ('hubspot')\n# and a.provider IN ('salesloft')\n# and c.id NOT IN (70)\n# and a.duration > 30\n# and actual_start_time > '2026-02-05 00:00:00'\norder by a.id desc;\n\nSELECT * FROM activities WHERE id = 37549787;\nSELECT * FROM crm_profiles WHERE user_id = 17613;\n\nSELECT * FROM crm_configurations WHERE id = 70;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 93 and sa.provider = 'hubspot';\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations WHERE id = 373; # KPSBremen.de 465 # - no social account\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 465 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 494;\n\nSELECT * FROM teams WHERE name LIKE '%splose%'; # 572, 495, 18708\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 572 and sa.provider = 'pipedrive';\n\nselect * from opportunities where team_id = 572\n# and name like '%Onebright%'\n# and is_closed = 1 and is_won = 0\n order by id desc;\n\n\nselect * from users where deleted_at is null and status = 2;\n\nselect * from contacts where id = 17900517;\nselect * from accounts where id = 10109838;\nselect * from opportunities where id = 6955880;\n\nselect * from opportunity_contacts where opportunity_id = 6955880;\nselect * from opportunity_contacts where contact_id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nSELECT * FROM activities WHERE uuid_to_bin('adcb8331-5988-4353-834e-383a355abba2') = uuid; # 38056424, crm 104659682404\nselect * from teams where id = 456;\nSELECT * FROM crm_configurations WHERE id = 363;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 456 and sa.provider = 'hubspot';\n\nselect * from crm_layouts where crm_configuration_id = 363;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (1203, 1204, 1635);\nSELECT * FROM crm_fields WHERE id IN (181536, 181538, 213455);\n\nSELECT * FROM teams WHERE name LIKE '%Electric%'; # 342, 272, 12767\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and name like 'NORTHUMBRIA POL%'; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 order by remotely_created_at asc; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and updated_at > '2026-01-01 00:00:00';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 272 and object_type = 'opportunity';\nSELECT * FROM crm_field_values WHERE crm_field_id = 127164;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\n\nSELECT * FROM teams WHERE id = 472;\nSELECT * FROM crm_configurations WHERE id = 380;\nselect * from activities where id = 38285673; # 38285673\nSELECT * FROM users WHERE id = 16942;\nSELECT * FROM groups WHERE id = 1964;\nSELECT * FROM playbooks WHERE id = 2033;\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 499; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1678;\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\n\nSELECT * FROM activities WHERE uuid_to_bin('96b1261f-2357-49f9-ab38-23ce12008ea0') = uuid;\n\nselect * from contacts c\nwhere c.crm_configuration_id = 370 order by c.updated_at desc;\n\nSELECT * FROM participants where activity_id = 38833541;\nSELECT * FROM participants where activity_id = 39216301;\nSELECT * FROM activity_summary_logs where activity_id = 39216301;\nSELECT * FROM activities WHERE uuid_to_bin('c7d99fbe-1fb1-41f2-8f4d-52e2bf70e1e9') = uuid; # 38833541, crm 478116564181\nSELECT * FROM activities WHERE uuid_to_bin('2e6ff4d3-9faa-447a-a8c1-9acde4d885ae') = uuid; # 39216301, crm 480171536586\nselect * from crm_profiles where crm_configuration_id = 319 and crm_provider_id = 525785080;\nselect * from opportunities where crm_configuration_id = 319 and crm_provider_id = 410150124747;\nselect * from accounts where crm_configuration_id = 319 and crm_provider_id = 47150650569;\nselect * from contacts where crm_configuration_id = 319 and crm_provider_id IN ('665587441856', '742723347700');\n# owner 13236 525785080\n# contact 1 16779180 665587441856 - activity - Alex Howes alex@supportroom.com created 2026-01-26\n# contact 2 19247563 742723347700 - ash@supportroom.com 2026-03-24\n# company 4176133 47150650569\n# deal 7100953 410150124747\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 400 and sa.provider = 'hubspot';\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556; # owner: 18101, crm: 477\nselect * from crm_configurations where id = 477;\nSELECT * FROM users WHERE id = 18101;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'integration-app';\n\nselect * from opportunities where id = 7594349;\nselect * from opportunity_stages where opportunity_id = 7594349 order by created_at desc;\nselect * from business_processes where id = 6024;\nselect * from business_process_stages where stage_id = 16352;\nselect * from business_process_stages where business_process_id = 6024;\nselect * from stages where team_id = 459;\nselect * from teams where id = 459;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 459 and sa.provider = 'hubspot';\n\nSELECT os.stage_id, s.crm_provider_id, s.name, COUNT(*) as cnt\nFROM opportunity_stages os\nJOIN stages s ON s.id = os.stage_id\nWHERE os.opportunity_id = 7594349\nGROUP BY os.stage_id, s.crm_provider_id, s.name\nORDER BY cnt DESC;\n\nSELECT s.id, s.crm_provider_id, s.name, s.team_id, s.crm_configuration_id\nFROM stages s\nJOIN business_process_stages bps ON bps.stage_id = s.id\nWHERE bps.business_process_id = 6024\nAND s.crm_provider_id = 'contractsent';\n\nselect * from stages where id IN (16352,20612,18281,7344,16378,16309,5036,15223,14535,6293,12098,11607)\n\nSELECT * FROM teams WHERE name LIKE '%Pulsar Group%'; # 472, 380, 15138, raza.gilani@vuelio.com\nselect * from playbooks where team_id = 472; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 2288;\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 380;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 472 and sa.provider = 'salesforce';\n\nselect * from activities where id = 58081273;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2;\n\nSELECT * FROM users WHERE name LIKE '%Neil Hoyle%'; # 17651\nSELECT * FROM social_accounts WHERE sociable_id = 17651;\n\nSELECT * FROM activities WHERE uuid_to_bin('975c6830-7d49-4c1e-b2e9-ac80c10a738a') = uuid;\nSELECT * FROM opportunities WHERE id IN (7842553, 6211727);\nSELECT * FROM contacts WHERE id IN (10202724, 6211727);\nSELECT * FROM opportunity_stages WHERE opportunity_id = 7842553;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 519 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 436;\nselect * from crm_profiles where crm_configuration_id = 436; # 76091797 -> 16612\n\nselect * from contact_roles where contact_id = 10202724;\n\nselect * from stages where team_id = 519; # 18778\n18775\n\nSELECT\n id,\n crm_provider_id,\n stage_id,\n is_closed,\n is_won,\n stage_updated_at,\n updated_at\nFROM opportunities\nWHERE id IN (6211727, 7842553);\n\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id = 6211727 AND contact_id = 10202724;\n\nSELECT id, name, stage_id, is_closed, is_won, updated_at, remotely_created_at\nFROM opportunities\nWHERE account_id = 8179134\nORDER BY updated_at DESC;\n\n\nselect * from text_relays where created_at > '2026-01-01';\nAND id IN (691, 692);\n\nselect * from teams;\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = a.user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nSELECT DISTINCT u.id, u.email, u.name, u.team_id, t.name as team_name,\n t.twilio_sms_sid, t.twilio_messaging_sid\nFROM users u\nINNER JOIN teams t ON u.team_id = t.id\nWHERE (t.twilio_sms_sid IS NOT NULL OR t.twilio_messaging_sid IS NOT NULL)\nAND u.status = 1\nORDER BY t.name, u.email;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 187 and sa.provider = 'salesforce';\n\nselect * from activities where id = 31264367;\nselect * from contacts where id = 6331639;\nselect * from accounts where id = 4156632;\nselect * from opportunities where id = 4843610;\n# update `activities` set `account_id` = 4156632, `contact_id` = 6331639, `opportunity_id` = 4843610,\n# `stage_id` = 13273, `activities`.`updated_at` = 2026-05-22 07:16:17 where `id` = 31264367)\"\n\nselect * from text_relays where created_at > '2026-05-01';\n\nselect * from activities order by id desc;\n\nselect * from users where name like '%Subra%';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('04a9cfad-2c87-4453-9e72-20aeb78ccf8d') = uuid;\nselect * from teams where id = 555;\nselect * from stages where team_id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 100 and sa.provider = 'hubspot';\n\nselect id, is_closed, is_won, stage_updated_at, crm_provider_id, stage_id, probability,\nclose_date, forecast_category, deleted_at, created_at, remotely_created_at, updated_at\nfrom opportunities where team_id = 555 and stage_id = 20616 order by updated_at desc limit 10;\n\nselect id, crm_provider_id, name, probability, updated_at, deleted_at\nfrom stages\nwhere id = 20616;","depth":4,"on_screen":true,"value":"SELECT * FROM team_features where team_id = 1;\n\nSELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922\nSELECT * FROM users WHERE team_id = 340; # 12015\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 340\nand sa.provider = 'salesforce';\n# and sa.provider = 'salesloft';\n\nselect * from crm_fields where crm_configuration_id = 270 and object_type = 'event';\n# 125558 - Event Type - Event_Type__c\n# 125552 - Event Status - Event_Status__c\n\nSELECT * FROM sidekick_settings WHERE team_id = 340;\n\nSELECT * FROM crm_field_values WHERE crm_field_id in (125552);\n\nselect * from activities where crm_configuration_id = 270\nand type = 'conference' and crm_provider_id IS NOT NULL\nand actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;\n\nSELECT * FROM activities WHERE id = 20871677;\nSELECT * FROM crm_field_data WHERE activity_id = 20871677;\n\nselect * from crm_layouts where crm_configuration_id = 270;\nselect * from crm_layout_entities where crm_layout_id in (886,887);\n\nSELECT * FROM crm_configurations WHERE id = 270;\n\nselect * from playbooks where team_id = 340; # 1514\nselect * from groups where team_id = 340;\nSELECT * FROM crm_fields WHERE id IN (125393, 125401);\n\nselect g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g\njoin playbooks p on g.playbook_id = p.id\njoin crm_fields f on p.activity_field_id = f.id\nwhere g.team_id = 340;\n\nSELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716\nselect * from crm_field_data where object_id = 20448716;\n\nselect * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008\nselect * from opportunities where team_id = 343;\nselect * from opportunities where team_id = 343 and crm_provider_id = '18099102526';\nselect * from opportunities where team_id = 343 and account_id = 945217482;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from accounts where team_id = 343 order by name asc;\n\nselect * from stages where crm_configuration_id = 273 and type = 'opportunity';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143\nSELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;\nSELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';\nSELECT * FROM activities WHERE id = 20717903;\n\nselect * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 353\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, l.atkinson@mwbsolutions.co.uk\nSELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;\n# id: 20940638, user: 12022, contact: 5305871\nSELECT * FROM activity_summary_logs WHERE activity_id = 20940638;\nselect * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 345\nand sa.provider = 'hubspot';\n\nselect * from users where team_id = 345 and id = 12022;\nSELECT * FROM crm_profiles WHERE user_id = 12022;\nSELECT * FROM participants WHERE activity_id = 20940638;\nSELECT * FROM users u\nJOIN crm_profiles cp ON u.id = cp.user_id\nWHERE u.team_id = 345;\n\nselect * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871\n\nselect * from team_features where team_id = 345;\nSELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197\nSELECT * FROM participants WHERE activity_id = 20897406;\n\n\n\nSELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912\nSELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';\n\n\nSELECT * FROM activities WHERE id = 20946641;\nSELECT * FROM crm_profiles WHERE user_id = 10211;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, triger@lunio.ai\nSELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';\nselect * from stages where crm_configuration_id = 97 and type = 'opportunity';\nselect * from opportunities where team_id = 120;\n\n\nselect * from crm_configurations crm join teams t on crm.id = t.crm_id\nwhere 1=1\nAND t.current_billing_plan IS NOT NULL\nAND crm.auto_sync_activity = 0\nand crm.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,james.lewendon@exclaimer.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 270\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956\nSELECT * FROM crm_profiles WHERE user_id = 11446;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, alex.chikly@cygnetise.com\nselect * from playbooks where team_id = 372;\nselect * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340\nSELECT * FROM crm_field_values WHERE crm_field_id = 141340;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 372\nand sa.provider = 'salesforce';\n\nselect * from crm_profiles where crm_configuration_id = 300;\nSELECT * FROM crm_configurations WHERE team_id = 372;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,mfa@planday.com\nSELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756\nselect * from crm_field_data where object_id = 3207756;\nSELECT * FROM crm_fields WHERE id = 111834;\n\nselect f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value\nFROM crm_fields f\nJOIN crm_field_data fd ON f.id = fd.crm_field_id\nWHERE f.crm_configuration_id = 242\nAND f.object_type = 'opportunity'\nAND fd.object_id IN (3207756)\nORDER BY fd.object_id, fd.updated_at;\n\nSELECT * FROM crm_configurations WHERE auto_connect = 1;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,salesforce-admin@tourlane.com\nselect * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id\nwhere g.team_id = 187;\n\nselect * from `groups` where team_id = 187;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 187\nand sa.provider = 'salesforce';\n\n# Destination - 98870 - Destination__c\n# Stage - 79014 - StageName\n# Land Arrangement - 98856 - Land_Arrangement__c\n# Flight - 98848 - Flight__c\n# Last activity date - 98812 - LastActivityDate\n# Last modified date - 98809 - LastModifiedDate\n# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c\n# next call - 98864 - Next_Call__c\n\nselect * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\nselect * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';\nselect * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;\nselect * from activities where opportunity_id = 3538248;\n\nSELECT * FROM crm_profiles WHERE user_id = 8150;\n\nselect * from deal_risks where opportunity_id = 3538248;\n\nselect * from teams where crm_id IS NULL;\n\nSELECT opp.id AS opportunity_id,\n u.group_id AS group_id,\n MAX(\n CASE\n WHEN a.type IN (\"sms-inbound\", \"sms-outbound\") THEN a.created_at\n ELSE a.actual_end_time\n END) as last_date\nFROM opportunities opp\nleft join activities a on a.opportunity_id = opp.id\ninner join users u on opp.user_id = u.id\nwhere opp.user_id IN (9951)\n\nAND opp.is_closed = 0\nand a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL\ngroup by opp.id;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,polly.morphew@cybsafe.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 301;\nSELECT * FROM contacts WHERE id = 6612363;\nSELECT * FROM accounts WHERE id = 4235676;\nSELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;\nselect * from opportunity_stages where opportunity_id = 4503759;\n# SELECT * FROM opportunities WHERE id = 4569937;\n\nselect * from activities where crm_configuration_id = 301;\nSELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370\nSELECT * FROM participants WHERE activity_id = 26330370;\n\nSELECT * FROM teams WHERE id = 375;\nselect * from playbooks where team_id = 375;\n\nselect * from stages where crm_configuration_id = 301 and type = 'opportunity';\n\nselect * from teams;\nselect * from contact_roles;\n\nSELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';\n\nselect * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;\n\nSELECT * FROM crm_field_data WHERE object_id = 3771706;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'\nand crm_provider_id LIKE \"%traffic_light%\";\nSELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);\n\nSELECT fd.* FROM opportunities o\nJOIN crm_field_data fd ON o.id = fd.object_id\nWHERE o.team_id = 343\n# and o.user_id IS NOT NULL\nand fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)\nand fd.value != ''\norder by value desc\n# group by o.id\n;\n\nSELECT * FROM opportunities WHERE id = 3769843;\n\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, salesforce-admin@tourlane.com\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,aswini.mishra@fundingcircle.com\nSELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839\n\n\nSELECT * FROM opportunities WHERE id = 3855992;\n\nSELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988\n\nSELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';\n\nselect * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507\nSELECT * FROM crm_field_data WHERE object_id = 5874411;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379\nand sa.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, nikhil.kumar@mention-me.com\nSELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\nSELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793\nselect * from generic_ai_prompts where subject_id = 3537793;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, triger@lunio.ai\nSELECT * FROM crm_configurations WHERE id = 97;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 97;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;\nSELECT * FROM crm_fields WHERE id = 32682;\n\nselect cfd.value, o.* from opportunities o\njoin crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682\nwhere team_id = 120\nand cfd.value != ''\n;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 120\nand sa.provider = 'salesforce';\n\nselect * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';\nSELECT * FROM crm_field_data WHERE object_id = 2313439;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 410;\nSELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';\nselect * from scorecards where team_id = 410;\nselect * from scorecard_rules;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, aswini.mishra@fundingcircle.com\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\njoin users u on o.user_id = u.id\nwhere a.crm_configuration_id = 177 and a.type LIKE '%email-out%'\n# and a.actual_end_time > '2024-12-16 00:00:00'\n# and o.remotely_created_at > '2024-12-01 00:00:00'\n# and u.group_id = 1014\nand u.id = 9021\norder by a.id desc;\nSELECT * FROM opportunities WHERE id in (3981384,4017346);\nSELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);\n\nselect * from users where id = 9021;\nselect * from inboxes where user_id = 9021;\n\nselect * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';\n\nselect * from email_messages where team_id = 220\nand orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'\nand subject LIKE '%Personal%'\n# and 'from' = 'credit@fundingcircle.com'\n;\n\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\nwhere a.user_id = 9021 and a.type LIKE '%email-out%'\nand a.actual_end_time > '2024-12-18 00:00:00'\nand o.user_id IS NOT NULL\nand o.remotely_created_at > '2024-12-01 00:00:00'\norder by a.id desc;\n\nSELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;\nselect * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;\n\nselect * from team_settings where name IN ('useCloseDate');\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, jfarrell@hurree.co\nSELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 104\nand sa.provider = 'hubspot';\n\nselect * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'\nselect * from teams where crm_id IS NULL;\n\nselect t.name as 'team', u.name as 'owner', u.email, u.phone\nfrom teams t\njoin activity_providers ap on t.id = ap.team_id\njoin users u on t.owner_id = u.id\nwhere 1=1\n and t.status = 'active'\n and ap.is_enabled = 1\n# and u.status = 1\n and ap.provider = 'ms-teams';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nSELECT * FROM teams WHERE id = 442; # 14293\nselect * from users where team_id = 442;\nselect * from social_accounts sa where sa.sociable_id = 14293;\nselect * from invitations where team_id = 442;\n\n# ********************************************************************************************************\nSELECT * FROM users WHERE email LIKE '%nea.liikamaa@eletive.com%'; # 14022\nSELECT * FROM teams WHERE id = 429;\nselect * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);\nselect * from activities where opportunity_id in (4340436,4353519);\n\nselect * from transcription where activity_id IN (25630961,25381771);\nselect * from generic_ai_prompts where subject_id IN (4353519);\n\nSELECT\n a.id as activity_id,\n a.opportunity_id,\n a.type as activity_type,\n a.language,\n CONCAT(a.title, a.description) AS mail_content,\n e.from AS mail_from,\n e.to AS mail_to,\n e.subject AS mail_subject,\n e.body AS mail_body,\n p.type as prompt_type,\n p.status as prompt_status,\n p.content AS prompt_content,\n a.actual_start_time as created_at\nFROM activities a\n LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL\n LEFT JOIN email_messages e ON a.id = e.activity_id\nWHERE a.actual_start_time > '2024-01-01 00:00:00'\n AND a.opportunity_id IN (4353519)\n AND a.status IN ('completed', 'received', 'delivered')\n AND a.deleted_at IS NULL\n AND a.type NOT IN ('sms-inbound', 'sms-outbound')\nORDER BY a.opportunity_id ASC, a.id ASC;\n\nSELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293\nSELECT * FROM teams WHERE id = 442;\nSELECT * FROM crm_configurations WHERE id = 344;\nselect * from team_features where team_id = 442;\nselect * from groups where team_id = 442;\nselect * from playbooks where team_id = 442;\nselect * from playbook_categories where playbook_id = 1729;\nselect * from crm_fields where crm_configuration_id = 344 and id = 172024;\nSELECT * FROM crm_field_values WHERE crm_field_id = 172024;\nselect * from crm_layouts where crm_configuration_id = 344;\nselect * from playbook_layouts where playbook_id = 1729;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444\n\nselect s.*\n# , s.sent_at, u.name, a.*\nfrom activity_summary_logs s\ninner join activities a on a.id = s.activity_id\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 356\nand s.sent_at > date_sub(now(), interval 60 day)\norder by a.actual_end_time desc;\n\nselect * from activities a\n# inner join activity_summary_logs s on s.activity_id = a.id\nwhere a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)\n# and a.crm_provider_id is not null\n# and provider <> 'ringcentral'\nand status = 'completed'\norder by a.actual_end_time desc;\n\nselect * from teams order by id desc; # 17328, 32, 17830, integration-account@jiminny.com\nSELECT * FROM users;\nSELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active\nSELECT * FROM teams WHERE id = 260;\nselect * from team_settings where team_id = 260;\nselect * from crm_configurations where team_id = 260;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 356;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;\n\nselect * from accounts where crm_configuration_id = 221 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 221 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 221 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 221 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 221;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 221 order by id desc;\nselect * from stages where crm_configuration_id = 221 order by id desc;\n\nselect * from accounts where crm_configuration_id = 356 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 356 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 356 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 356 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 356;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 356 order by id desc;\nselect * from stages where crm_configuration_id = 356 order by id desc;\n\nselect * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)\nselect * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)\nselect * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4\nselect ce.* from calendars c\njoin users u on c.user_id = u.id\njoin calendar_events ce on c.id = ce.calendar_id\nwhere u.team_id = 260\nand (ce.start_time > '2025-02-21 00:00:00')\n;\n# calendar events 1207\n#\n\nselect * from opportunities where team_id = 260;\nSELECT * FROM crm_field_data WHERE object_id = 4696496;\n\nselect * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;\nselect * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')\n# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0\nand created_at > '2024-03-01 00:00:00'\norder by id desc; # 880 000, ringcentral, avaya\nSELECT * FROM participants WHERE activity_id = 26371744;\n\n# all activities 942 000 +\n# conference 7385 - scheduled 984 - external 343\n\nselect * from activities where id = 26321812;\nselect * from participants where activity_id = 26321812;\nselect * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);\nselect * from leads where id in (720428,689175,731546,645866,621037);\n\nselect * from users where id = 13841;\nselect * from opportunities where user_id = 9541;\nselect * from stages where id = 15900;\n\nselect * from accounts where\n# id IN (4160055,5053725,4965303,4896434)\nid in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)\n;\n\nselect * from activities where id = 26654935;\nSELECT * FROM opportunities WHERE id = 4803458;\n\nSELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;\nSELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time\nFROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);\n\nSELECT DISTINCT\n o.id, o.stage_id, s.name, a.title,\n a.*\nFROM activities a\n# INNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nINNER JOIN groups g ON u.group_id = g.id\nINNER JOIN opportunities o ON a.opportunity_id = o.id\nINNER JOIN stages s ON o.stage_id = s.id\nWHERE\n a.crm_configuration_id = 356\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 13841\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')\n AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')\n\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n )\n )\n AND (\n# s.id = 15900\n s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')\n OR s.uuid IS NULL -- Include records without opportunity stage\n )\n\nORDER BY a.actual_end_time DESC;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, willsc@leadforensics.com\nSELECT * FROM users WHERE team_id = 190;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 190\nand sa.provider = 'hubspot';\n\nselect * from role_user where user_id = 8474;\n\nselect * from crm_configurations where provider = 'bullhorn';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;\nSELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;\n\nSELECT * FROM opportunities WHERE id = 4732493;\nselect * from activities where opportunity_id = 4732493;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 443; # 358, 14315, andrea.romano@correrenaturale.com\nSELECT * FROM opportunities WHERE team_id = 443;\n\nSELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id\nFROM activities AS a\nJOIN stages AS s ON a.stage_id = s.id\nJOIN users AS u ON u.id = a.user_id\nJOIN teams AS t ON t.id = s.team_id\nWHERE u.team_id <> s.team_id and t.id > 135;\n\n\nSELECT\n crm_configuration_id,\n crm_provider_id,\n COUNT(*) as duplicate_count,\n GROUP_CONCAT(id) as stage_ids,\n GROUP_CONCAT(name) as stage_names\nFROM stages\nGROUP BY crm_configuration_id, crm_provider_id\nHAVING COUNT(*) > 1\nORDER BY duplicate_count DESC;\n\nselect * from stages where id IN (14898,14907);\n\nselect * from business_processes;\n\nSELECT *\nFROM crm_configurations\nWHERE team_id IN (\n SELECT team_id\n FROM crm_configurations\n GROUP BY team_id\n HAVING COUNT(*) > 1\n)\nORDER BY team_id;\n\nSELECT *\nFROM teams\nWHERE crm_id IN (\n SELECT crm_id\n FROM teams\n GROUP BY crm_id\n HAVING COUNT(*) > 1\n)\nORDER BY crm_id;\n\n# ***************************************************************************\nselect * from crm_configurations where provider = 'integration-app';\nSELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 andrea.romano@correrenaturale.com\nselect * from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect * from team_features where team_id = 358;\nselect * from activity_summary_logs;\n\nselect * from teams where id = 406;\n\n# ************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, srv.salesforce@sportfive.com\nselect * from activities where crm_configuration_id = 202 order by actual_end_time desc;\n\nSELECT * FROM users where id = 14637;\nSELECT * FROM teams where id = 267;\nSELECT * FROM groups where id = 1118;\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 202\n AND status IN ('completed', 'failed')\n AND recording_state != 'stopped'\n AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n AND (is_private = 0 OR user_id = 14637)\n AND (\n (\n actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n ) OR (\n actual_start_time IS NULL\n AND type IN ('sms-outbound', 'sms-inbound')\n AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tracks\n WHERE\n tracks.activity_id = activities.id\n AND tracks.type IN ('audio', 'video')\n )\nORDER BY actual_end_time DESC;\n\nSELECT DISTINCT\n a.*\nFROM activities a\nINNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nWHERE\n a.crm_configuration_id = 202\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 14637\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND a.user_id = 14637\n )\n )\n\nORDER BY a.actual_end_time DESC\n;\n\nSELECT DISTINCT a.*\nFROM activities a\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams t ON u.team_id = t.id\n# INNER JOIN tracks tr ON a.id = tr.activity_id\n# INNER JOIN groups g ON u.group_id = g.id\nWHERE 1=1\n AND t.id = 267\n# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND tr.type NOT IN ('audio', 'video')\n AND (\n a.is_private = 0\n OR a.user_id = 14637\n )\n AND (\n (a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')\n OR (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'\n )\n )\n# and NOT EXISTS (\n# SELECT 1\n# FROM tracks t\n# WHERE t.activity_id = a.id\n# AND t.type IN ('audio', 'video')\n# )\n\nORDER BY a.actual_end_time DESC;\n\nSELECT * FROM tracks WHERE activity_id = 26485995;\n\nselect a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 202\n# and a.is_internal = 0\nand (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type IN (\"softphone\",\"softphone-inbound\",\"conference\",\"sms-inbound\")\nand a.status IN ('completed', 'failed')\n# and a.external_id is not null\norder by a.actual_end_time desc;\n\nselect * from activities a where a.crm_configuration_id = 202\nand a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'\n# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_field_data WHERE crm_field_id = 98809;\n\nselect * from users where status = 1 AND timezone = 'MDT';\n\nselect * from opportunities where id = 3769814;\nselect * from deal_risks where opportunity_id = 3769814;\n\nselect cp.* from crm_profiles cp\njoin users u on cp.user_id = u.id\njoin crm_configurations crm on cp.crm_configuration_id = crm.id\nwhere crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';\n\nselect * from crm_fields where id = 154575;\n\nselect * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';\nSELECT * FROM teams WHERE id = 176; # crm 148\nselect * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nselect * from crm_fields cf\njoin crm_configurations crm on crm.id = cf.crm_configuration_id\nwhere crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');\n\n# *********************************************************************************************\nSELECT * FROM users WHERE id IN (15415, 15418);\nSELECT * FROM groups WHERE id IN (1805,1806);\nSELECT * FROM playbooks WHERE id = 1860;\nSELECT * FROM playbook_categories WHERE id = 38634;\nSELECT * FROM crm_fields WHERE id = 189962;\n\nSELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 raza.gilani@vuelio.com\n\nSELECT * FROM crm_profiles WHERE user_id = 15415;\nSELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';\n\nselect * from sidekick_settings where team_id = 472;\n\nSELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418\nSELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415\n\n# *********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, salesforce-integrations@teamtailor.com\nselect * from crm_configurations where id = 218;\nSELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765\nSELECT * FROM users WHERE id IN (13232, 13230);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n0057R00000EPL5HQAX Inez Ekblad\n\n1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur\n\nSELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);\n\n############################################################################################\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id IN (94491,94493,94498);\nSELECT * FROM users WHERE id = 13658;\nSELECT * FROM teams WHERE id = 109;\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, katy.holden@strengthscope.comk\nSELECT * FROM stages WHERE crm_configuration_id = 390;\nselect * from business_processes where team_id = 481 and crm_configuration_id = 390;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 481\nand sa.provider = 'salesforce';\n\n\nSELECT * FROM users WHERE id = 15780; # team 462\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 462\nand sa.provider = 'hubspot';\n\n\nselect * from teams where id = 495;\nSELECT * FROM users WHERE id = 15794;\nselect * from social_accounts where sociable_id = 15794;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752\nSELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794\nSELECT * FROM activities WHERE crm_configuration_id = 407\nand status = 'completed' and type = 'conference'\norder by id desc;\n\nselect ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id\njoin permission_role pr on pr.role_id = ru.role_id\n join permissions p on p.id = pr.permission_id\nwhere team_id = 495 and p.name IN ('dial');\n\nselect * from permission_role;\n\nselect * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;\nSELECT * FROM activities WHERE id = 29512773;\nSELECT * FROM activities WHERE id IN (29042721,28991325,29002874);\n\nSELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 407\n# and a.id IN (29042721,28991325,29002874);\n\nSELECT * FROM users WHERE id = 15794;\nSELECT * FROM users WHERE team_id = 495;\nSELECT * FROM social_accounts WHERE sociable_id = 15794;\nSELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';\nSELECT * FROM contacts WHERE team_id = 495;\nSELECT * FROM leads WHERE team_id = 495;\nSELECT * FROM accounts WHERE team_id = 495;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 407;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 407;\nSELECT * FROM crm_configurations WHERE id = 407;\nSELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'\nand user_id IS NOT NULL and is_closed = 1 and is_won = 1;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103\nSELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064\nSELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');\n\n# *********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 325\nand sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085\nSELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733\nSELECT * FROM activity_summary_logs where activity_id = 28719733;\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444\nSELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';\nSELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630\nselect * from activities where crm_configuration_id = 356 and lead_id = 841732;\n\nSELECT * from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 356;\n\nselect * from activities where crm_configuration_id = 356\nand actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'\norder by id desc;\n\nselect * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;\nselect * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\n\nselect * from team_features where team_id = 260;\nselect * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;\n\nselect * from crm_fields;\nselect * from crm_layout_entities;\n\nSELECT * FROM teams WHERE name LIKE '%Optable%';\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id in (94491,94493,94498);\n\nselect * from teams where crm_id IS NULL;\n\nSELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;\n\n# *************************************************************************************************\nselect * from team_domains where team_id = 399;\nSELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207\n\nselect * from calendar_events where id = 5163781;\nSELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896\nSELECT * FROM participants WHERE activity_id = 29443896;\nselect * from contacts where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\nselect * from leads where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\n\nselect * from activities where user_id = 14937 order by created_at ;\n\nselect * from users where id = 14937;\n\nselect * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';\nselect * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';\n\nselect * from activities a join participants p on a.id = p.activity_id\nwhere crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';\n\n# *************************************************************************************************\nSELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';\nSELECT * FROM opportunities WHERE team_id = 379 order by id desc;\nSELECT * FROM teams WHERE id = 379;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379 and sociable_id = 13852\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE id = 307;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 307;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307\n and id IN (144750,144855,145158,155227);\n\nSELECT * FROM activities;\n\n\nselect * from activities\nwhere created_at > '2025-07-01 00:00:00'\n# and created_at < '2025-08-01 00:00:00'\nand type not in ('email-outbound', 'email-inbound')\nand account_id is null\nand contact_id is null\nand lead_id is null\nand opportunity_id is not null\n;\nSELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);\nSELECT * FROM crm_configurations WHERE id in (335,301,200);\n\nselect * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';\n\nSELECT * FROM teams WHERE name LIKE '%Resights%';\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nselect * from teams where id IN (442);\n\nselect * from activities\nwhere crm_configuration_id = 177\nand provider = 'amazon-connect'\n order by id desc;\n# and source <> 'gong';\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nSELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;\n\n\nselect * from crm_configurations where store_transcript = 1;\nSELECT * FROM teams WHERE id IN (80);\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 277\nand sa.provider = 'salesforce';\n\nselect * from activities where crm_configuration_id = 213 and account_id = 2511502;\n\nselect * from crm_configurations where id = 213;\n\nSELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604\nSELECT * FROM participants WHERE activity_id = 33981604;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 431\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223\nselect * from activity_summary_logs where activity_id = 33997223;\nselect * from activity_notes where activity_id = 33997223;\n\n# ***********************************\nSELECT * FROM teams WHERE name LIKE '%Abode%';\n\n\nselect * from features;\nselect * from teams t\nwhere t.status = 'active'\nand id NOT IN (select team_id from team_features where feature_id = 9)\n;\n\n\nselect * from playbook_layouts where playbook_id = 1725;\nSELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473\nselect * from teams where id = 318;\nselect * from crm_configurations where team_id = 318;\nselect * from playbooks where team_id = 318;\nSELECT * FROM crm_layouts where crm_configuration_id = 381;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;\nSELECT * FROM crm_fields WHERE id IN (192938,192936,192939);\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;\nSELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);\n\nSELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533\n\n\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nSELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);\n\nSELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124\nselect * from crm_layouts where crm_configuration_id = 281;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nselect * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);\nselect * from opportunities where crm_configuration_id = 281;\n\nSELECT * FROM activities WHERE id IN (34211315, 34130075);\nSELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);\n\nselect cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd\njoin crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id\njoin crm_fields cf on cle.crm_field_id = cf.id\nwhere cf.deleted_at IS NOT NULL\nGROUP BY cle.id, cf.id;\n\nselect * from crm_layouts where id IN (355);\nselect u.email, t.crm_id, t.* from teams t\njoin users u on u.id = t.owner_id\nwhere crm_id IN (97);\n\nSELECT * FROM crm_fields WHERE id = 96492;\n\nselect * from permissions;\nselect * from permission_role where permission_id = 247;\nselect * from roles;\n\nselect * from migrations;\n# *****************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('291e3c21-11cc-4728-aee7-6e4bedf86d72') = uuid; # 34262174\nSELECT * FROM crm_configurations WHERE id = 301;\nSELECT * FROM teams WHERE id = 343;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from participants where activity_id = 34262174;\n\nselect * from contacts where crm_configuration_id = 301 and id = 6976326;\nselect * from accounts where crm_configuration_id = 301 and id IN (4647626, 4815829); # 30761335403\n\nselect * from activity_summary_logs where activity_id = 34262174;\n\nselect * from users where status = 1 AND timezone = 'EST';\n\n# ****************************************************************************\nSELECT * FROM users WHERE id = 13869;\nSELECT * FROM crm_configurations WHERE id = 320;\nSELECT * FROM teams WHERE id = 401;\n\nSELECT * FROM activities WHERE uuid_to_bin('2228c16f-10be-48d5-90d4-67385219dc01') = uuid; # 29670601\n\nSELECT * FROM accounts WHERE id = 7761483;\nSELECT * FROM opportunities WHERE id = 6051814;\n\nSELECT * FROM teams WHERE name LIKE '%Seedlegals%';\n\n;select * from opportunities where updated_at > '2025-10-11' AND crm_provider_id = '34713761166';\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 177;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 577;\nSELECT * FROM crm_fields WHERE id IN (68458,68459,68480,68497,68524,68530,68554,68618,68662,68781,68810,68898,68981,69049,97467);\n\nSELECT t.id, crm.id, t.name, crm.sync_objects, crm.provider, crm.last_synced_at FROM crm_configurations crm join teams t on t.crm_id = crm.id\nwhere t.status = 'active' AND crm.provider = 'hubspot' AND crm.last_synced_at < '2025-10-22 00:00:00';\n\nSELECT * FROM activities WHERE uuid_to_bin('fa09449f-cba9-496a-b8f3-865cd3c72351') = uuid;\nSELECT * FROM crm_configurations where id = 184;\nSELECT * FROM teams WHERE id = 246;\nSELECT * FROM social_accounts WHERE sociable_id = 9259 and provider = 'hubspot';\n\nSELECT * FROM users WHERE email LIKE '%rhian.old@bud.co.uk%'; # 17700\nSELECT * FROM teams WHERE id = 551;\n\nSELECT * FROM crm_configurations WHERE id = 471;\nSELECT * FROM activities WHERE crm_configuration_id = 471 and crm_provider_id IS NOT NULL;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 471;\nSELECT * FROM crm_fields WHERE id = 307260;\nSELECT * FROM crm_field_values WHERE crm_field_id = 307260;\n\nselect * from crm_layouts where crm_configuration_id = 471;\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1547;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1548;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 551 and sa.provider = 'hubspot';\n\nSELECT * FROM teams WHERE name LIKE '%$PCS%';\n\n# ********************************************************************************************************\nselect * from crm_configurations crm\njoin teams t on t.crm_id = crm.id\nwhere t.status = 'active'\nand crm.provider = 'hubspot';\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 crm_configurations where id = 331; # 416\nSELECT * FROM teams WHERE id = 416;\nSELECT * FROM opportunities WHERE team_id = 190;\n\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%';\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 190 and sa.provider = 'hubspot';\n\n\n\nSELECT * FROM teams WHERE name LIKE '%Rapaport%'; # 431, 337\nSELECT * FROM teams where id = 431;\nSELECT * FROM crm_configurations where team_id = 431;\nSELECT * FROM activity_providers where team_id = 431;\nSELECT * FROM activities where crm_configuration_id = 337 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 431 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%BiP%'; # 401, 320\nSELECT * FROM teams where id = 401;\nSELECT * FROM crm_configurations where team_id = 401;\nSELECT * FROM activity_providers where team_id = 401;\nSELECT * FROM activities where crm_configuration_id = 320 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 401 and sa.provider = 'salesforce';\n\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 307; # 379 - Story Terrace Inc , portalId: 3921157\nSELECT * FROM contacts WHERE team_id = 379 and updated_at > '2026-01-31 11:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 379 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; # 563 - LATUS Group (ad94d501-5d09-44fd-878f-ca3a9f8865c3) , portalId: 3904501\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 338; # 432 - Formalize , portalId: 9214205\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 432 and sa.provider = 'hubspot';\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 436; # 519 - Moxso , portalId: 25531989\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 96; # 119 - Nourish Care , portalId: 26617984\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 331; # 416 - The National College , portalId: 7213852\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 308; # 380 - Foodles , portalId: 7723616\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 379; # 471 - imat-uve , portalId: 9177354\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 465; # 545 - Spotler , portalId: 144759271\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 455; # 537 - indevis , portalId: 25666868\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 200; # 265 - Jobadder , portalId: 6426676\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 335; # 429 - Eletive , portalId: 6110563\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 363; # 456 - Global Group , portalId: 8901981\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 297; # 369 - Unbiased , portalId: 9229005\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 353; # 449 - Fuuse , portalId: 25781745\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 487; # 566 - Nimbus , portalId: 39982590\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 487;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1630;\nselect * from crm_fields where crm_configuration_id = 487 and\n(uuid_to_bin('4c6b2971-64d4-45b8-b377-427be758b5a5') = uuid or uuid_to_bin('59e368d8-65a0-4b77-b611-db37c99fbe68') = uuid);\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 420; # 506 - voiio , portalId: 145629154\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 479; # 558 - Momice , portalId: 535962\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 59; # 80 - Storyclash GmbH , portalId: 4268479\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 175; # 203 - Team iAM , portalId: 5534732\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 368; # 460 - OneTouch Health , portalId: 5534732183355\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\n\n\nselect * from users where id = 29643;\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM teams WHERE name LIKE '%Buynomics%'; # 462, 482, 14910\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\n# and description like '%The call focused on understanding Welch%'\norder by id desc;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 462 and sa.provider = 'salesforce';\n\nselect * from contacts where crm_configuration_id = 482 and name = 'Cyndall Hill'; # 15504749\nselect * from contacts where id = 10891096; # 482\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\nand contact_id = 15504749\norder by id desc;\n\nselect * from activities where id = 36793003; # 96cc7bc1-8622-4d27-92f4-baf664fc1a56, 00UOf00000PDdOXMA1\nselect * from transcription where id = 7646782;\nselect * from ai_prompts where transcription_id = 7646782;\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7a8471a3-847e-4822-802b-ddf426bbc252') = uuid; # 37370018\nSELECT * FROM activity_summary_logs WHERE activity_id = 37370018;\nSELECT * FROM teams WHERE id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 555 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7c17b8aa-09df-4f85-a0f7-51f47afd712d') = uuid; # 37395250\nSELECT * FROM activities WHERE uuid_to_bin('14d60388-260d-494b-aa0d-63fdb1c78026') = uuid; # 37395250\n\nSELECT a.* FROM activities a JOIN crm_configurations c on c.id = a.crm_configuration_id\nwhere a.type IN ('softphone', 'softphone-outbound') and c.provider = 'hubspot'\nand a.provider NOT IN ('hubspot')\n# and a.provider IN ('salesloft')\n# and c.id NOT IN (70)\n# and a.duration > 30\n# and actual_start_time > '2026-02-05 00:00:00'\norder by a.id desc;\n\nSELECT * FROM activities WHERE id = 37549787;\nSELECT * FROM crm_profiles WHERE user_id = 17613;\n\nSELECT * FROM crm_configurations WHERE id = 70;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 93 and sa.provider = 'hubspot';\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations WHERE id = 373; # KPSBremen.de 465 # - no social account\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 465 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 494;\n\nSELECT * FROM teams WHERE name LIKE '%splose%'; # 572, 495, 18708\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 572 and sa.provider = 'pipedrive';\n\nselect * from opportunities where team_id = 572\n# and name like '%Onebright%'\n# and is_closed = 1 and is_won = 0\n order by id desc;\n\n\nselect * from users where deleted_at is null and status = 2;\n\nselect * from contacts where id = 17900517;\nselect * from accounts where id = 10109838;\nselect * from opportunities where id = 6955880;\n\nselect * from opportunity_contacts where opportunity_id = 6955880;\nselect * from opportunity_contacts where contact_id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nSELECT * FROM activities WHERE uuid_to_bin('adcb8331-5988-4353-834e-383a355abba2') = uuid; # 38056424, crm 104659682404\nselect * from teams where id = 456;\nSELECT * FROM crm_configurations WHERE id = 363;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 456 and sa.provider = 'hubspot';\n\nselect * from crm_layouts where crm_configuration_id = 363;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (1203, 1204, 1635);\nSELECT * FROM crm_fields WHERE id IN (181536, 181538, 213455);\n\nSELECT * FROM teams WHERE name LIKE '%Electric%'; # 342, 272, 12767\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and name like 'NORTHUMBRIA POL%'; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 order by remotely_created_at asc; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and updated_at > '2026-01-01 00:00:00';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 272 and object_type = 'opportunity';\nSELECT * FROM crm_field_values WHERE crm_field_id = 127164;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\n\nSELECT * FROM teams WHERE id = 472;\nSELECT * FROM crm_configurations WHERE id = 380;\nselect * from activities where id = 38285673; # 38285673\nSELECT * FROM users WHERE id = 16942;\nSELECT * FROM groups WHERE id = 1964;\nSELECT * FROM playbooks WHERE id = 2033;\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 499; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1678;\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\n\nSELECT * FROM activities WHERE uuid_to_bin('96b1261f-2357-49f9-ab38-23ce12008ea0') = uuid;\n\nselect * from contacts c\nwhere c.crm_configuration_id = 370 order by c.updated_at desc;\n\nSELECT * FROM participants where activity_id = 38833541;\nSELECT * FROM participants where activity_id = 39216301;\nSELECT * FROM activity_summary_logs where activity_id = 39216301;\nSELECT * FROM activities WHERE uuid_to_bin('c7d99fbe-1fb1-41f2-8f4d-52e2bf70e1e9') = uuid; # 38833541, crm 478116564181\nSELECT * FROM activities WHERE uuid_to_bin('2e6ff4d3-9faa-447a-a8c1-9acde4d885ae') = uuid; # 39216301, crm 480171536586\nselect * from crm_profiles where crm_configuration_id = 319 and crm_provider_id = 525785080;\nselect * from opportunities where crm_configuration_id = 319 and crm_provider_id = 410150124747;\nselect * from accounts where crm_configuration_id = 319 and crm_provider_id = 47150650569;\nselect * from contacts where crm_configuration_id = 319 and crm_provider_id IN ('665587441856', '742723347700');\n# owner 13236 525785080\n# contact 1 16779180 665587441856 - activity - Alex Howes alex@supportroom.com created 2026-01-26\n# contact 2 19247563 742723347700 - ash@supportroom.com 2026-03-24\n# company 4176133 47150650569\n# deal 7100953 410150124747\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 400 and sa.provider = 'hubspot';\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556; # owner: 18101, crm: 477\nselect * from crm_configurations where id = 477;\nSELECT * FROM users WHERE id = 18101;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'integration-app';\n\nselect * from opportunities where id = 7594349;\nselect * from opportunity_stages where opportunity_id = 7594349 order by created_at desc;\nselect * from business_processes where id = 6024;\nselect * from business_process_stages where stage_id = 16352;\nselect * from business_process_stages where business_process_id = 6024;\nselect * from stages where team_id = 459;\nselect * from teams where id = 459;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 459 and sa.provider = 'hubspot';\n\nSELECT os.stage_id, s.crm_provider_id, s.name, COUNT(*) as cnt\nFROM opportunity_stages os\nJOIN stages s ON s.id = os.stage_id\nWHERE os.opportunity_id = 7594349\nGROUP BY os.stage_id, s.crm_provider_id, s.name\nORDER BY cnt DESC;\n\nSELECT s.id, s.crm_provider_id, s.name, s.team_id, s.crm_configuration_id\nFROM stages s\nJOIN business_process_stages bps ON bps.stage_id = s.id\nWHERE bps.business_process_id = 6024\nAND s.crm_provider_id = 'contractsent';\n\nselect * from stages where id IN (16352,20612,18281,7344,16378,16309,5036,15223,14535,6293,12098,11607)\n\nSELECT * FROM teams WHERE name LIKE '%Pulsar Group%'; # 472, 380, 15138, raza.gilani@vuelio.com\nselect * from playbooks where team_id = 472; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 2288;\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 380;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 472 and sa.provider = 'salesforce';\n\nselect * from activities where id = 58081273;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2;\n\nSELECT * FROM users WHERE name LIKE '%Neil Hoyle%'; # 17651\nSELECT * FROM social_accounts WHERE sociable_id = 17651;\n\nSELECT * FROM activities WHERE uuid_to_bin('975c6830-7d49-4c1e-b2e9-ac80c10a738a') = uuid;\nSELECT * FROM opportunities WHERE id IN (7842553, 6211727);\nSELECT * FROM contacts WHERE id IN (10202724, 6211727);\nSELECT * FROM opportunity_stages WHERE opportunity_id = 7842553;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 519 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 436;\nselect * from crm_profiles where crm_configuration_id = 436; # 76091797 -> 16612\n\nselect * from contact_roles where contact_id = 10202724;\n\nselect * from stages where team_id = 519; # 18778\n18775\n\nSELECT\n id,\n crm_provider_id,\n stage_id,\n is_closed,\n is_won,\n stage_updated_at,\n updated_at\nFROM opportunities\nWHERE id IN (6211727, 7842553);\n\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id = 6211727 AND contact_id = 10202724;\n\nSELECT id, name, stage_id, is_closed, is_won, updated_at, remotely_created_at\nFROM opportunities\nWHERE account_id = 8179134\nORDER BY updated_at DESC;\n\n\nselect * from text_relays where created_at > '2026-01-01';\nAND id IN (691, 692);\n\nselect * from teams;\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = a.user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nSELECT DISTINCT u.id, u.email, u.name, u.team_id, t.name as team_name,\n t.twilio_sms_sid, t.twilio_messaging_sid\nFROM users u\nINNER JOIN teams t ON u.team_id = t.id\nWHERE (t.twilio_sms_sid IS NOT NULL OR t.twilio_messaging_sid IS NOT NULL)\nAND u.status = 1\nORDER BY t.name, u.email;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 187 and sa.provider = 'salesforce';\n\nselect * from activities where id = 31264367;\nselect * from contacts where id = 6331639;\nselect * from accounts where id = 4156632;\nselect * from opportunities where id = 4843610;\n# update `activities` set `account_id` = 4156632, `contact_id` = 6331639, `opportunity_id` = 4843610,\n# `stage_id` = 13273, `activities`.`updated_at` = 2026-05-22 07:16:17 where `id` = 31264367)\"\n\nselect * from text_relays where created_at > '2026-05-01';\n\nselect * from activities order by id desc;\n\nselect * from users where name like '%Subra%';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('04a9cfad-2c87-4453-9e72-20aeb78ccf8d') = uuid;\nselect * from teams where id = 555;\nselect * from stages where team_id = 555;\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 on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 100 and sa.provider = 'hubspot';\n\nselect id, is_closed, is_won, stage_updated_at, crm_provider_id, stage_id, probability,\nclose_date, forecast_category, deleted_at, created_at, remotely_created_at, updated_at\nfrom opportunities where team_id = 555 and stage_id = 20616 order by updated_at desc limit 10;\n\nselect id, crm_provider_id, name, probability, updated_at, deleted_at\nfrom stages\nwhere id = 20616;","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}]...
|
7945821021870316922
|
-537191020546090905
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
1
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'ac...
|
88383
|
NULL
|
NULL
|
NULL
|
|
88383
|
3013
|
50
|
2026-05-28T17:00:38.673728+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779987638673_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity, but pull request details loading failed","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.85638297,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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":"Show Replace Field","depth":4,"bounds":{"left":0.11868351,"top":0.15482841,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.13131648,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"existingStages","depth":4,"bounds":{"left":0.14228724,"top":0.15403032,"width":0.043882977,"height":0.015961692},"on_screen":true,"value":"existingStages","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.19514628,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.20511968,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.2137633,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.22240691,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/2","depth":4,"bounds":{"left":0.23603724,"top":0.15323225,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.26163563,"top":0.15243416,"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":"Next Occurrence","depth":4,"bounds":{"left":0.27027926,"top":0.15243416,"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":"Filter Search Results","depth":4,"bounds":{"left":0.2789229,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.28756648,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.46210107,"top":0.15243416,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5290066260295796825
|
-8538472745289002240
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
existingStages
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
rapstomCoocFV faVsco.|s ~#12121 on JY-20963-fx-lProinet vHubspotClientinterface.ph© HubspotTokenManager.pt© PayloadBuilder.phpoKimol CimosewhooResponseNormalize.phoCSeMCPono© SyncFieldAction.ohoexisiinastaad10.02.23 Vasilev© synckelateoncuvnymanas 24.01.25 Papazov© WebhookSvncBatchProce17.03 25 ianlisteners> MetadataaMicrationP oedriveEh SalesforceafeldsaOpoortunityVatchenOpportunitySyncStrategyProsoec SaarchStrateosametiteê crant nhoC DecorateActivity.php( DeletcObiectsTrait.phposwan ntone non© PayloadBuilder.phpc) Profile.php© QueryBuilder.php© QueryHandler.php© Queryiterator.php© QueryResults.php© Service.php© SyncBatchRedisService.ptin TraitsRaseeentonoCrmActivityProviderinteorateCCnlACMiWMoh©rmobiectctesower.onC. DefaultProsoectSearchStrateC mallteloer.ond@. FindeProsoectinterfacc.ohoC) LavouMansoe ono16.0425 wanok710552.0418204.18Graham19.03.18 Grahan2.0418Olanidi20.10.21 Grahan710202.04.18Grahan2.10.252.10.258.11.182.04.18Graham19.03.18 Grahan4.05.264.05.264.05.26405.262.10.297-1025A05.28A05.28A05.28C Opportun tvActvitwlatcheeei lennortur tevnaCtestomiedirnenont tschd nhrHe OrnenontCostrhSrond nhnWindowClostoeas noseiveioe40$«11[PHONE]38440442446448450sraveloe© RecordSelector.phgC) ACUVIY.or.PC) Team.phd# HS local [liminny@localhostA console (EU) x iii users (EU)console (STAGINGclass Service extends BaseService implements01 A7 A149 V1V33 /1 A v 170%public function importStages(?array Stypes = null, ?string SnissingStageNane = null): ?Stage1706— 1708tean_idnamel=> Sthis->tean->id,3>nhstrirwidthSol"abelestart: 8,wiohSen171612111"type"=> BusinessPnocess: :TYPE_OPPORTUNITY,=> Sp['active'),1713A record type is really a clone of the business process, used to store which record use-/eCreate/update record type cloneSthis->config->recordTypes@->update0rCreatel'cre provider 1d' => Sp('id')— 1715-1728= 1726'tean id= Scns->cean->10,=> mb strinwidth(Sp('label').=> Sp('active')"business process_id' => SbusinessProcess->id ?? nullwidth: 158)172:= schis->cont 10=>scageslo->withTrashedO->where('type', Stage:: TYPE_OPPORTUNITY)->kevßv("con croviden id'),=172≤1727=1729=173017391E17321733oneach astaops.asheasradeSs= ResponseNonnalize::normaLizeleal Staae(SdealStace),=1735/** @vac ?Stage SexistingStage *,SeyistainoStaopSoyiistainaStaaesosaet/Sch.d// Restore soft-deleted stages that are now active in HubSpot1f (SexistingStage?->trashed() && $s['active']) (SeyistnoStanp.sractoneoE17571738= 17551703=174=1742= 1143/ Upsert stage (updates soft-deleted records without restoring thenSstage = Sthis->confiq->stages@->withTrashed@->update0rCreate"crnprovider id' => Ss(id'.= 1743—1/40—Cahtedwheda= mb_strinwidth(Ss("label'= mb_strinwidth(Ssf"label'MAN 1041Tx: AutovSo liminnyvBROER PYTnane, M.emare031 A9 A29 V3 /109 A VSELECT * FRON tEaNS WHERE name LIKE "Stounlanes: # 187, 289, 8158SEEiNTCONCAT(U.1d, CASE WHEN U.10 = t.ouner_1d THEN" (owner)" ELSEMrenasisa.*t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.soclable_idJOIN teans t 1.nc->1: on t.id = u.team_1cWHERE u.tean_id = 187 and sa.provider = 'salesfonce':select * fron activities where id = 31264367select * fron contacts where id = 6331639:seleekx tron account anchs 0-450054select * fron opportunities where id = 4843610:#Uodare#'stage_id' = 132730c0un8-4300-4"contact_id' ="updated at" = 2826-95-22 07:16:select * trot1text relavs where created at > *2026-95-01°select * fron actviales order oy 1o desciselect * fron users where nane oike SubrasaSELECT * FROM opportunities WHERE wnid to_bin('04a9cfad-2c87-4453-$|select * fron teans where 1d= 555%select * fron stages where tean_1d = 555:SETSTMTCONCAT(U.Ld, CASE WHEN U.1d = t.ouner_id THEN" (owner)" ELSEhonnen siil coesasccounteeJOIN users u on u.id = sa.sociable.igahittinnetneosonettsWHERE u.tean_id = 100 and sa.provider = 'hubspot":select id, is_closed, is_won, stage_updated.at, crm_provider_id, stclose date, forecast_category, deleted_at, created_at, remotely_crtfron opportunities where tean id = SSS and stage_id = 28616 orderselect id. crn_provider id, name,probabilitv, undated at, deletesfron stageswhere id = 28616TO0У L7Inu Lo moy kuiuuiseServiceTestCascadeles Orcnnworeebeeionhomwwtino ooooDatelr Luk Fe0 27 16:87318 2826 -020831t. ComDlewor retum deletedisoor+0.© aft choy 8714617963 -100/Carufcec/Con/Hubsnot/Sarusce.ohdDate:Nhu bếy 5 122215k0 282: 1820vSync Hubspot Active Dealsshdex aittedea/ Stvse/T bepot/Servie, php b/spg/Services/Crm/hubspot/Servie.phpNow lct me look at the Nov 2025 buix import = what inportStages looked like at that timo• git log-oneline -beforea*2025-11-19" -after"2025-09-01" -- app/Services/Crm/Hubspot/Service.php | head -S• git shơw 874C3cea56 - app/Services/Crm/Hubspot/Service-php | grep -A 48 "inportStages• oft show 5f6720fd5bsano/Saryices/Crm/Hubspot /Service,oho I areo -A 59 "function StroortStages"public function inportStages(array Stypes & null, ?string SaissingStagelane • null): 75taggSmissingStage = nullspipe lineskesponse # sthis-setient-sgecinsteceuoooecotenierroudecnooothiesCommand ait. areo, tal0 git show 5fd720fdSb:app/Services/Crm/Hubspot/Service-php | grep -A 70 "function importStages" | tail -25Ask anything (XOL)tylwiew oulteonaet today RayRun st= (Skip• OuwnderhimtKaawhirest4 spad...
|
NULL
|
NULL
|
NULL
|
NULL
|