|
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
|
|
85261
|
2921
|
10
|
2026-05-28T12:15:54.926050+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970554926_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
29 matches in 8 files
File mask:
*.p Find in Files
29 matches in 8 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/tests/Unit/Services/Mail
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Component/AiActivityType/Services...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"29 matches in 8 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.042220745,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiActivityType/Services","depth":6,"on_screen":false,"role_description":"text"}]...
|
-8513371325110625743
|
-5049823808272104445
|
visual_change
|
accessibility
|
NULL
|
Find in Files
29 matches in 8 files
File mask:
*.p Find in Files
29 matches in 8 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
$lockAcquired = Redis::set(self::POLLING_LOCK_KEY, json_encode($lockData), 'EX', self::LOCK_TTL_SECONDS, 'NX');
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/tests/Unit/Services/Mail
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Component/AiActivityType/Services...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85260
|
2921
|
9
|
2026-05-28T12:15:52.552186+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970552552_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
SIUCKWindowHelpPwovscors#12121 on JY-20963-folProi SIUCKWindowHelpPwovscors#12121 on JY-20963-folProinet vecanyice pn.he bolbees.phe©ActivityControlier.php© VoiceConsentPrefix.phpNotificationsnObservers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc(in Policie:aProviders• Queuea Reoositonea RulesaServicesDa ActivityAiReodrleallendarConnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyMAetioneMcontndSuncstrateon• D DTO› FieldsThtauendOLeAkоkyscrco.onOneindi.onoMWiaKorCass wochexeR82* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)Opportunivsvncstratcov- Pac nation- Prosoec-SearchStrateo→- RedisService traits• Utils- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCleantono@ GiosedDealStaapsSeniceScacheKey = Sthis->getRateLimitCacheKeyOScacheocxozresac = keozst.oet scachekeyif (is_string(ScachedExpiresAt) && is_numeric(ScachedExpiresAt)) 1Sremaining = naxl va(cint)) ScachedExpiresAt - tineon:hrow new kacetinzcexceoezonsrenaining,neruonSantwaittecarch leinronabieoi4 (Sthiso>isHubsootRateLinit(Se))thisosparse?erny.er e// NX: only the first job to receive a 429 in this burst sets the key./l Subsequent 429s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redis::set(ScacheKey, (string) (tine() + SretryAfter).options:'EX', SretryAfter, "NX*);Sthis->log->warning( Hubspot) Received 429 from API'.iteanSdl'configid'= Sthis->config->getido.=) Sratmultten-ENAeMAONNATthrow new RateLinitException message: "Hubspot returned 429'. SretryAfter. Se):Throw Senclnaalise rccarica onoprivate function getRateLinitCachekevO: strinol...;=custom.loglaravel.logA console (STAGING)-782783704711[712713[PHONE]22.722723725727729E732-73.—73073- 738)TX: AUTONAND a.created_at › DATE_:GROUP BY u.id, u.email,ORDER BY sns_count DESCselect + Eron teans, wherlselect + Eron coles,SELECTCONCATU.Id, CASE WHsa.*t.ouner id FROM sociJOIN users u on u.id = sJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM actávitioSELECT * FROM actávitioSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenseleetron miawhonkeselect + fron olavbookseleet ron eoeselect * fron crn_field.SELECT * FROM crn_field_AOTM con Brelds 4 0M 4• JOIN activities a ON $NHERF Actvity "d = 7002ANn & con nrovsden 3alSELECT * FROM activity.mselect * fron text relayselect * fron activitiesselect * fron users whenPARAtT GAAR MANGUREANOPAlRetGhAn dAAAIRdAMselect & fron teans wherPohattoon activtysHoeDMsActivityJiminnY ….# backend# buescontusion-dinig# donut_time#enpineerine# general# happy birthdavA infocoe intemal all# infra-chansed# infrastructure de#bu-team-infoiliminnv-bost olatform-team# platform-ticketstoroduct launche# random# release# suppor# thank-youssie the neoole of fimill6? Direct message.Vasil VasilenF8. Stefka Stoyanova1a llivana NetsevaAa Stovan TomovPetko Kashinski( Galva Dimitrova P: Todor Stamator1 Stelivan Georsie.D Vod&r MireNikolav Vanko&. James GrahamLukas Kovalky#:AppsS lin Gloudinu co moy to:loro@ Describe what you are looking forVasil Vasileve MessagerNoR0d canvis7 Files+Vasil Vasilev 2:56 PM1 с добър коментарDoory2e intentionaЗ сьщо e intentional поради факта, че phpredis dniver-a не поддьржа set ex +nх параметри, и тряова да се пуснат две операции set + expireLukas Kovalik 3.07 PMок. зз по пуснах пак поез gеminіlза третата точка обаче наистина ли няма подьржкааз май си ги ползвам някьлеVasil Vasilev 3:10 PMне става, бях го направил така, и се счупиLukas Kovalik 3:10 PMами и при мен се чумешеVasil Vasilev 3:11 PMpredis поддьржа set(Skey. Svalue. nx' - true, ex =60)Cnhnranic He caRaLukas Kovalik 3.14pNтака май не работеше Sresult = Redis=set(Stestkey, StestValue, "ax, Sttl, "NX*); а така да Sresult = Redis:set(StestKey, StestValue, [nx, 'ex' => Sttl)):Vasil Vasiley 2.11 pwLukas Kovalik 3.12 pNне помно BеUEVasil Vasilev 3:12 PMobоитноtо може nаeLukas Kovalik 3-12 PMможе и ла брькамVasil Vasilev 3:12 PMewons uusзначи пьрвото работиLukas Kovalik 313 PMmaueann/Somicac/crm/ithsnor/lciontnon..Vasil Vasiley 314 pvwe to ononnsMessage Vasil Vasiley...
|
NULL
|
1203767077918883082
|
NULL
|
click
|
ocr
|
NULL
|
SIUCKWindowHelpPwovscors#12121 on JY-20963-folProi SIUCKWindowHelpPwovscors#12121 on JY-20963-folProinet vecanyice pn.he bolbees.phe©ActivityControlier.php© VoiceConsentPrefix.phpNotificationsnObservers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc(in Policie:aProviders• Queuea Reoositonea RulesaServicesDa ActivityAiReodrleallendarConnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyMAetioneMcontndSuncstrateon• D DTO› FieldsThtauendOLeAkоkyscrco.onOneindi.onoMWiaKorCass wochexeR82* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)Opportunivsvncstratcov- Pac nation- Prosoec-SearchStrateo→- RedisService traits• Utils- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCleantono@ GiosedDealStaapsSeniceScacheKey = Sthis->getRateLimitCacheKeyOScacheocxozresac = keozst.oet scachekeyif (is_string(ScachedExpiresAt) && is_numeric(ScachedExpiresAt)) 1Sremaining = naxl va(cint)) ScachedExpiresAt - tineon:hrow new kacetinzcexceoezonsrenaining,neruonSantwaittecarch leinronabieoi4 (Sthiso>isHubsootRateLinit(Se))thisosparse?erny.er e// NX: only the first job to receive a 429 in this burst sets the key./l Subsequent 429s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redis::set(ScacheKey, (string) (tine() + SretryAfter).options:'EX', SretryAfter, "NX*);Sthis->log->warning( Hubspot) Received 429 from API'.iteanSdl'configid'= Sthis->config->getido.=) Sratmultten-ENAeMAONNATthrow new RateLinitException message: "Hubspot returned 429'. SretryAfter. Se):Throw Senclnaalise rccarica onoprivate function getRateLinitCachekevO: strinol...;=custom.loglaravel.logA console (STAGING)-782783704711[712713[PHONE]22.722723725727729E732-73.—73073- 738)TX: AUTONAND a.created_at › DATE_:GROUP BY u.id, u.email,ORDER BY sns_count DESCselect + Eron teans, wherlselect + Eron coles,SELECTCONCATU.Id, CASE WHsa.*t.ouner id FROM sociJOIN users u on u.id = sJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM actávitioSELECT * FROM actávitioSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenseleetron miawhonkeselect + fron olavbookseleet ron eoeselect * fron crn_field.SELECT * FROM crn_field_AOTM con Brelds 4 0M 4• JOIN activities a ON $NHERF Actvity "d = 7002ANn & con nrovsden 3alSELECT * FROM activity.mselect * fron text relayselect * fron activitiesselect * fron users whenPARAtT GAAR MANGUREANOPAlRetGhAn dAAAIRdAMselect & fron teans wherPohattoon activtysHoeDMsActivityJiminnY ….# backend# buescontusion-dinig# donut_time#enpineerine# general# happy birthdavA infocoe intemal all# infra-chansed# infrastructure de#bu-team-infoiliminnv-bost olatform-team# platform-ticketstoroduct launche# random# release# suppor# thank-youssie the neoole of fimill6? Direct message.Vasil VasilenF8. Stefka Stoyanova1a llivana NetsevaAa Stovan TomovPetko Kashinski( Galva Dimitrova P: Todor Stamator1 Stelivan Georsie.D Vod&r MireNikolav Vanko&. James GrahamLukas Kovalky#:AppsS lin Gloudinu co moy to:loro@ Describe what you are looking forVasil Vasileve MessagerNoR0d canvis7 Files+Vasil Vasilev 2:56 PM1 с добър коментарDoory2e intentionaЗ сьщо e intentional поради факта, че phpredis dniver-a не поддьржа set ex +nх параметри, и тряова да се пуснат две операции set + expireLukas Kovalik 3.07 PMок. зз по пуснах пак поез gеminіlза третата точка обаче наистина ли няма подьржкааз май си ги ползвам някьлеVasil Vasilev 3:10 PMне става, бях го направил така, и се счупиLukas Kovalik 3:10 PMами и при мен се чумешеVasil Vasilev 3:11 PMpredis поддьржа set(Skey. Svalue. nx' - true, ex =60)Cnhnranic He caRaLukas Kovalik 3.14pNтака май не работеше Sresult = Redis=set(Stestkey, StestValue, "ax, Sttl, "NX*); а така да Sresult = Redis:set(StestKey, StestValue, [nx, 'ex' => Sttl)):Vasil Vasiley 2.11 pwLukas Kovalik 3.12 pNне помно BеUEVasil Vasilev 3:12 PMobоитноtо може nаeLukas Kovalik 3-12 PMможе и ла брькамVasil Vasilev 3:12 PMewons uusзначи пьрвото работиLukas Kovalik 313 PMmaueann/Somicac/crm/ithsnor/lciontnon..Vasil Vasiley 314 pvwe to ononnsMessage Vasil Vasiley...
|
85257
|
NULL
|
NULL
|
NULL
|
|
85251
|
2920
|
5
|
2026-05-28T12:14:56.617722+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970496617_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
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 ...
|
[{"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":"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,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
-3264450658369633090
|
1136716483744153188
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
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 ...
|
85250
|
NULL
|
NULL
|
NULL
|
|
85250
|
2920
|
4
|
2026-05-28T12:14:55.793467+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970495793_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.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:14:55181ec2-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
|
-6914738773934183330
|
NULL
|
idle
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeRefactorRunToolsGi PhpStormFileEditViewNavigateCodeRefactorRunToolsGitWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O ₴4‹$0(ah]100% <78• Thu 28 May 15:14:55181ec2-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
|
NULL
|
NULL
|
NULL
|
|
85249
|
2921
|
4
|
2026-05-28T12:14:26.861977+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970466861_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground...
|
[{"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":"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.3587101,"top":0.15003991,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.36801863,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"bounds":{"left":0.37799203,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","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;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
7844122297473728914
|
5225835645296838756
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85248
|
2921
|
3
|
2026-05-28T12:14:24.869300+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970464869_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
SIUCKWindowHelpPwovscors#12121 on JY-20963-folProi SIUCKWindowHelpPwovscors#12121 on JY-20963-folProinet vecanyice pn.he bolbees.phe©ActivityControlier.php© VoiceConsentPrefix.phpNotificationsnObservers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.pho(in Policie:aProviders• Queuea Reoositonea RulesaServicesDa ActivityAiReodrleallendarConnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspol(i) AccountSyncStrategyMAetioneMcontndSuncstrateon• D DTO› FieldsThtauendOLeAkоkyscrco.onC Teamuprp183Opportunivsvncstratcov- Pac nation- Prosoec-SearchStrateo→- RedisService traits• Utils- Wechookc) Batchsunceorector cho© BatchSvncRedsService.oCleantonoCLeAAA Den CTadAcCenoeclnaalise rccarica onoOneindi.onoC) Client.php X php autoload.phdMWiaKorCass wochexebaseuoehraocathies wueswethheat* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)ScacheKey = Sthis->getRateLimitCacheKeyOScacheocxozresac = keozst.oet scachekeyif (is_string(ScachedExpiresAt) && is_numeric(ScachedExpiresAt)) 1Sremaining = naxl vax(cint)) ScachedExpiresAt - tineon:hrow new kaceLinzcexceoe.onsrenaining,reruonSant waittercarch ethronadteoi4 (Sthiso>isHubsootRateLinit(Se))thisosparse?erny.er e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 42%s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redis::set(ScacheKey, (string) (tine() + SretryAfter).options:'EX', SretryAfter, "NX*);app/Services/Crm/Hubspot/Client.php:183Sthis->log->warning( Hubspot) Received 429 from API'.iteanSdl"conffa Kd= Sthis->config->getido."retry after'as Scetryatter=> Se->getMessageOthrow new RateLinitException message: "Hubspot returned 429'. SretryAfter. Se):Throw Senprivate function getRateLinitCachekevO: strinol...;=custom.loglaravel.logA console [STAGING-782785704785786711[712713[PHONE]725727728729E732-733—730737- 738)TX: AUTONAND a.created_at › DATE_:GROUP BY u.id, u.email,ORDER BY sns_count DESCselect + Eron teans, wherlselect + Eron coles,CEIEOTCONCATU.Id, CASE WHsa.*t.ouner id FROM sociJOIN users u on u.id = sJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM actávitioSELECT * FROM actávitioSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenselect * fron playbooksseleetron miawhonkeselect * fron playbook_ctseleet ron eoeselect * fron crn_field.vSELECT * FROM crn_field_AOTM con Brelds 4 0M 4• JOIN activities a ON $NHERF Actvity "d = 7002AND € con onovider saSELECT * FROM activity.mselect * fron text relayselect * fron activitiesselect * fron users whenPARAtT GAAR MANGUREANOPAlRetGhAn dAAAIRdAMselect & fron teans wherPhat"toon actytysHoeDMsActivityMonJiminnY ….# backend# buescontusion-dinig# donut_time#enpineerine# general# happy birthdavA infocoe intemal all# infra-chansed# infrastructure de#bu-team-infoiliminnv-bo# platform-team# platform-ticketstoroduct launche# random# release# suppor# thank-youssie the neoole of fimill6? Direct message.Vasil VasilenF8. Stefka Stoyanova1a llivana NetsevaA Ctovan TomnulPetko KashinskiGola Nimitrous: Todor Stamator1 Stelivan Georsie.&r MireNikolav Vanko&. James GrahamLukas Kovalky7o0sLXInu cowoy 1o:14.2-@ Describe what you are looking forVasil Vasileve MessagerNoR0d canvis7 Files+Vasil Vasilev 2:56 PM1 с добър коментарDoory2e intentionaЗ сьщо e intentional поради факта, че phpredis dniver-a не поддьржа set ex +nх параметри, и тряова да се пуснат две операции set + expireLukas Kovalik 3.07 PMок. зз по пуснах пак поез gеminіlза третата точка обаче наистина ли няма подьржкааз май си ги ползвам някьлеVasil Vasilev 3:10 PMне става, бях го направил така, и се счупиLukas Kovalik 3:10 PMами и при мен се чумешеVasil Vasilev 3:11 PMpredis поддьржа set(Skey. Svalue. nx' - true, ex =60)Cnhnranic He caRaLukas Kovalik 3.14pNтака май не работеше Sresult = Redis=set(Stestkey, StestValue, "ax, Sttl, "NX*); а така да Sresult = Redis:set(StestKey, StestValue, [nx, 'ex' => Sttl)):Vasil Vasiley 2.11 pwLukas Kovalik 3.12 pNне помно BеUEVasil Vasilev 3:12 PMobоитноtо може nаeLukas Kovalik 3-12 PMможе и ла брькамVacil Vaciley 242 pwewons uusSlockAcquired - Redis::set(self::POLLING LOCK KEY, ison encode(SlockData). "EX', self::LOCK TTL SECONDS. *NX*))значи пьрвото работиLukas Kovalik 313 PMwe to ononns...
|
NULL
|
1963937319043274319
|
NULL
|
click
|
ocr
|
NULL
|
SIUCKWindowHelpPwovscors#12121 on JY-20963-folProi SIUCKWindowHelpPwovscors#12121 on JY-20963-folProinet vecanyice pn.he bolbees.phe©ActivityControlier.php© VoiceConsentPrefix.phpNotificationsnObservers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.pho(in Policie:aProviders• Queuea Reoositonea RulesaServicesDa ActivityAiReodrleallendarConnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspol(i) AccountSyncStrategyMAetioneMcontndSuncstrateon• D DTO› FieldsThtauendOLeAkоkyscrco.onC Teamuprp183Opportunivsvncstratcov- Pac nation- Prosoec-SearchStrateo→- RedisService traits• Utils- Wechookc) Batchsunceorector cho© BatchSvncRedsService.oCleantonoCLeAAA Den CTadAcCenoeclnaalise rccarica onoOneindi.onoC) Client.php X php autoload.phdMWiaKorCass wochexebaseuoehraocathies wueswethheat* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)ScacheKey = Sthis->getRateLimitCacheKeyOScacheocxozresac = keozst.oet scachekeyif (is_string(ScachedExpiresAt) && is_numeric(ScachedExpiresAt)) 1Sremaining = naxl vax(cint)) ScachedExpiresAt - tineon:hrow new kaceLinzcexceoe.onsrenaining,reruonSant waittercarch ethronadteoi4 (Sthiso>isHubsootRateLinit(Se))thisosparse?erny.er e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 42%s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redis::set(ScacheKey, (string) (tine() + SretryAfter).options:'EX', SretryAfter, "NX*);app/Services/Crm/Hubspot/Client.php:183Sthis->log->warning( Hubspot) Received 429 from API'.iteanSdl"conffa Kd= Sthis->config->getido."retry after'as Scetryatter=> Se->getMessageOthrow new RateLinitException message: "Hubspot returned 429'. SretryAfter. Se):Throw Senprivate function getRateLinitCachekevO: strinol...;=custom.loglaravel.logA console [STAGING-782785704785786711[712713[PHONE]725727728729E732-733—730737- 738)TX: AUTONAND a.created_at › DATE_:GROUP BY u.id, u.email,ORDER BY sns_count DESCselect + Eron teans, wherlselect + Eron coles,CEIEOTCONCATU.Id, CASE WHsa.*t.ouner id FROM sociJOIN users u on u.id = sJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM actávitioSELECT * FROM actávitioSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenselect * fron playbooksseleetron miawhonkeselect * fron playbook_ctseleet ron eoeselect * fron crn_field.vSELECT * FROM crn_field_AOTM con Brelds 4 0M 4• JOIN activities a ON $NHERF Actvity "d = 7002AND € con onovider saSELECT * FROM activity.mselect * fron text relayselect * fron activitiesselect * fron users whenPARAtT GAAR MANGUREANOPAlRetGhAn dAAAIRdAMselect & fron teans wherPhat"toon actytysHoeDMsActivityMonJiminnY ….# backend# buescontusion-dinig# donut_time#enpineerine# general# happy birthdavA infocoe intemal all# infra-chansed# infrastructure de#bu-team-infoiliminnv-bo# platform-team# platform-ticketstoroduct launche# random# release# suppor# thank-youssie the neoole of fimill6? Direct message.Vasil VasilenF8. Stefka Stoyanova1a llivana NetsevaA Ctovan TomnulPetko KashinskiGola Nimitrous: Todor Stamator1 Stelivan Georsie.&r MireNikolav Vanko&. James GrahamLukas Kovalky7o0sLXInu cowoy 1o:14.2-@ Describe what you are looking forVasil Vasileve MessagerNoR0d canvis7 Files+Vasil Vasilev 2:56 PM1 с добър коментарDoory2e intentionaЗ сьщо e intentional поради факта, че phpredis dniver-a не поддьржа set ex +nх параметри, и тряова да се пуснат две операции set + expireLukas Kovalik 3.07 PMок. зз по пуснах пак поез gеminіlза третата точка обаче наистина ли няма подьржкааз май си ги ползвам някьлеVasil Vasilev 3:10 PMне става, бях го направил така, и се счупиLukas Kovalik 3:10 PMами и при мен се чумешеVasil Vasilev 3:11 PMpredis поддьржа set(Skey. Svalue. nx' - true, ex =60)Cnhnranic He caRaLukas Kovalik 3.14pNтака май не работеше Sresult = Redis=set(Stestkey, StestValue, "ax, Sttl, "NX*); а така да Sresult = Redis:set(StestKey, StestValue, [nx, 'ex' => Sttl)):Vasil Vasiley 2.11 pwLukas Kovalik 3.12 pNне помно BеUEVasil Vasilev 3:12 PMobоитноtо може nаeLukas Kovalik 3-12 PMможе и ла брькамVacil Vaciley 242 pwewons uusSlockAcquired - Redis::set(self::POLLING LOCK KEY, ison encode(SlockData). "EX', self::LOCK TTL SECONDS. *NX*))значи пьрвото работиLukas Kovalik 313 PMwe to ononns...
|
85246
|
NULL
|
NULL
|
NULL
|
|
85247
|
2920
|
3
|
2026-05-28T12:14:24.758208+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970464758_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeRefactorRunToolsGi PhpStormFileEditViewNavigateCodeRefactorRunToolsGitWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O ₴4‹$0(ah]A100% <478• Thu 28 May 15:14:24181ec2-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
|
5311969006285717777
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeRefactorRunToolsGi PhpStormFileEditViewNavigateCodeRefactorRunToolsGitWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O ₴4‹$0(ah]A100% <478• Thu 28 May 15:14:24181ec2-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...
|
85245
|
NULL
|
NULL
|
NULL
|
|
85227
|
2918
|
65
|
2026-05-28T12:13:20.753141+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970400753_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.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...
|
[{"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}]...
|
-5714658179048184897
|
-8634297297851446848
|
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
TextRelayServiceTest
PhpStormFileEditViewNavigateCodeRefactorRunToolsGitWindowHelpec2-user@ip-10-30-129-190:~ffmpeg0 X4lahlA100% CThu 28 May 15:13:20181ec2-user@ip-10-30-140-...₴7DOCKER#_####_#####\\###||\#/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...
|
85225
|
NULL
|
NULL
|
NULL
|
|
85226
|
2919
|
70
|
2026-05-28T12:13:18.737215+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970398737_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
FV faVsco.is ~#12121 on JY-20963-fx-inProinet vnhe FV faVsco.is ~#12121 on JY-20963-fx-inProinet vnhe heloers phi©ActivityControlier.php© VoiceConsentPrefix.phpNotificationsnObservers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc(in Policie:uProviderC Queuea Reoositonea RulesaServicesDa ActivityAiReodrlealendarIiconnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyIhctiontMcontctSwnestrateon• D DTO> FieldsThtauendOLekоkyseiwico.on© Kernel.phgMWiaKorCass wochexeR82* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)Opportunivsvncstratcov- Pac nation- Prosoec-SearchStrateoScacheKey = Sthis->getRateLimitCacheKeyOScacheotxozresac = keozst.oet scachekey)*if (is_string(ScachedExpiresAt) && is_numeric(ScachedExpiresAt)) 1Sremaining = naxl vax(cint)) ScachedExpiresAt - tineon:hrow new kacetinzcexceoezonsrenaining,neruonSantwaittecarch ethronadteoi4 (Sthiso>isHubsootRateLinit(Se))merryAttenthisorrarse?erny.ter e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 42%s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redis::set(ScacheKey, (string) (tine() + SretryAfter).Sthis->log->warning( Hubspot) Received 429 from API'.iteanSdl'configid'= Sthis->config->getido.as Scetryatter-ENAeMAONNAT→- Redisthrow new RateLinitException message: "Hubspot returned 429'. SretryAfter, Se):Service traits• UtilsThrow Sen- Wechookc) Batchsunceo ector cho© BatchSvncRedsService.oCleantono@ GiosedDealStaapsSeniceclnaalise rccarica onoprivate function getRateLinitCachekey@: strino...100% L74oo Inu co moy lo:lo.+0.Setm onwatoes noeslA console (STAGING)D6.00€ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYITGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER RY sns count783704FREREДI714-215716select + Eron teans whenp dm15C3IEACONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN " (ouner)' ELSE "* END) AS user_id,t.ouner id FROM social accounts saJOIN users u on u.id = sa.sociable ic1.n<->1: on t.id = u.team_idd console (PROD) x C Service.php# console fiaumDojminnyv0458185Y0 ACacaado CodoxoKick of a new oroiset. Make chunosIMY"E729E732- 735* JOIN activities a ON fd.activity_id = a.1dWHERE activity_id = 79933459• AND f.crn_provider_id = 'hs_activity_type';id = 1 and id IN ((18688. 13934. 7160):Sid = 7168 order by id desc Linit 10:PARAtT GAAR MANGURAANOselect * fron teans where id = 1117select * fron activity searches where user_1d = 31054:Pohettendactivity search_filters where actlivity_search_id TN (88882. 88992)c. Fix Texikelay service TesteC Multi-Picklist Layout DisplaNtietnheetdor10o Podeswiett...
|
NULL
|
865779409316485795
|
NULL
|
click
|
ocr
|
NULL
|
FV faVsco.is ~#12121 on JY-20963-fx-inProinet vnhe FV faVsco.is ~#12121 on JY-20963-fx-inProinet vnhe heloers phi©ActivityControlier.php© VoiceConsentPrefix.phpNotificationsnObservers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc(in Policie:uProviderC Queuea Reoositonea RulesaServicesDa ActivityAiReodrlealendarIiconnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyIhctiontMcontctSwnestrateon• D DTO> FieldsThtauendOLekоkyseiwico.on© Kernel.phgMWiaKorCass wochexeR82* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)Opportunivsvncstratcov- Pac nation- Prosoec-SearchStrateoScacheKey = Sthis->getRateLimitCacheKeyOScacheotxozresac = keozst.oet scachekey)*if (is_string(ScachedExpiresAt) && is_numeric(ScachedExpiresAt)) 1Sremaining = naxl vax(cint)) ScachedExpiresAt - tineon:hrow new kacetinzcexceoezonsrenaining,neruonSantwaittecarch ethronadteoi4 (Sthiso>isHubsootRateLinit(Se))merryAttenthisorrarse?erny.ter e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 42%s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redis::set(ScacheKey, (string) (tine() + SretryAfter).Sthis->log->warning( Hubspot) Received 429 from API'.iteanSdl'configid'= Sthis->config->getido.as Scetryatter-ENAeMAONNAT→- Redisthrow new RateLinitException message: "Hubspot returned 429'. SretryAfter, Se):Service traits• UtilsThrow Sen- Wechookc) Batchsunceo ector cho© BatchSvncRedsService.oCleantono@ GiosedDealStaapsSeniceclnaalise rccarica onoprivate function getRateLinitCachekey@: strino...100% L74oo Inu co moy lo:lo.+0.Setm onwatoes noeslA console (STAGING)D6.00€ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYITGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER RY sns count783704FREREДI714-215716select + Eron teans whenp dm15C3IEACONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN " (ouner)' ELSE "* END) AS user_id,t.ouner id FROM social accounts saJOIN users u on u.id = sa.sociable ic1.n<->1: on t.id = u.team_idd console (PROD) x C Service.php# console fiaumDojminnyv0458185Y0 ACacaado CodoxoKick of a new oroiset. Make chunosIMY"E729E732- 735* JOIN activities a ON fd.activity_id = a.1dWHERE activity_id = 79933459• AND f.crn_provider_id = 'hs_activity_type';id = 1 and id IN ((18688. 13934. 7160):Sid = 7168 order by id desc Linit 10:PARAtT GAAR MANGURAANOselect * fron teans where id = 1117select * fron activity searches where user_1d = 31054:Pohettendactivity search_filters where actlivity_search_id TN (88882. 88992)c. Fix Texikelay service TesteC Multi-Picklist Layout DisplaNtietnheetdor10o Podeswiett...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85225
|
2918
|
64
|
2026-05-28T12:13:18.842111+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970398842_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeRefactorRunToolsGi PhpStormFileEditViewNavigateCodeRefactorRunToolsGit881DEV (-zsh)O 82-zshN3WindowHelpec2-user@ip-10-30-129-190:~ffmpeg€ 84DOCKER#_####_#####\\###||\#/V~'-zshlahlA100% (C47Thu 28 May 15:13:18181X5ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7Amazon Linux 2023 (ECSArtisanCLP Artisan_/m/Currency SymbolsFor documentation, visit [URL_WITH_CREDENTIALS] ~]$ exitlogoutConnection to jiminny-prod-ecs1 closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $lukas@Lukas-Kovaliks-MacBook-Pro-JiminnyWarning: Permanently added 'jiminny-prod-ecs:DatabasedatetimesFE logsGeneraljiminny debugMac symbolsPhpstormA newer release of "Amazon Linux" is availabPhpstorm classesVersion 2023.10.20260330:Version 2023.11.20260406:postman payloadsVersion 2023.11.20260413:Version2023.11.20260427:terminalVersion2023.11.20260505:User dataVersion2023.11.20260509:add field to sidekickVersion 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)₴82₴83₴4₴85286₴87288₴89View snippets for 'Artisan'Type to filter collections and snippets/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
|
-3440525658930420579
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeRefactorRunToolsGi PhpStormFileEditViewNavigateCodeRefactorRunToolsGit881DEV (-zsh)O 82-zshN3WindowHelpec2-user@ip-10-30-129-190:~ffmpeg€ 84DOCKER#_####_#####\\###||\#/V~'-zshlahlA100% (C47Thu 28 May 15:13:18181X5ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7Amazon Linux 2023 (ECSArtisanCLP Artisan_/m/Currency SymbolsFor documentation, visit [URL_WITH_CREDENTIALS] ~]$ exitlogoutConnection to jiminny-prod-ecs1 closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $lukas@Lukas-Kovaliks-MacBook-Pro-JiminnyWarning: Permanently added 'jiminny-prod-ecs:DatabasedatetimesFE logsGeneraljiminny debugMac symbolsPhpstormA newer release of "Amazon Linux" is availabPhpstorm classesVersion 2023.10.20260330:Version 2023.11.20260406:postman payloadsVersion 2023.11.20260413:Version2023.11.20260427:terminalVersion2023.11.20260505:User dataVersion2023.11.20260509:add field to sidekickVersion 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)₴82₴83₴4₴85286₴87288₴89View snippets for 'Artisan'Type to filter collections and snippets/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
|
NULL
|
NULL
|
NULL
|
|
85224
|
2919
|
69
|
2026-05-28T12:13:13.566320+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970393566_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.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'...
|
[{"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}]...
|
-1678849104122188672
|
593894980799643872
|
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
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
rnpstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-proidetwnapers.oh©ActivityControlier.php© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phcCActwwooserver.pnp© ContactObserver.phpGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc> (i PoliciesuProvider• Queuea Reoositonea RulesaServicesActivityAiReodrleallendarleoseConnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspol(i) AccountSyncStrategyIhctiontMcontdSuncstrateon• D DTO› FieldsThtauendTNMAAAatOLenkоkyseiwico.on© Kernel.phpMWiaKorCass wochexebaseuoehraocathies wueswetheheatR8R* Breturn T The result of the APi callOpportunivsvncstratcov> Ma pacinationaProsoec SearchStrateoprivate function executeRequest(callable SapiCall)ScacheKey = Sthis->getRateLimitCacheKeyOScacheotxozresac = keozst.oet scachekey)if (is_string(ScachedExpiresAt) && is_numeric(ScachedExpiresAt)) 1Sremaining = naxl vax(cint)) ScachedExpiresAt - tineon:hrow new kacetinzcexceoezonsrenaining,neruonSantwaittecarch leinronabieoi4 (Sthiso>isHubsootRateLinit(Se))merry…ttenthisorrarse?erny.ter e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 42%s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redisoeset(Scachekey. (strino) (tine() e Sratevafter).lilunsnate Sunnont Sacades RedssSthis->log->warning( Hubspot) Received 429 from API'.iteanSdl'configid'=> Sthis->config-sgetido."retry after' => SretryAften=> Se->getMessageO→- Redisthrow new RateLinitException message: "Hubspot returned 429'. SretryAfter, Se):Service traits• UtilsThrow Sen- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCileantono@ GiosedDealStadpsSeniceclnaalise rccarica onoprivate function getRateLinitCachekevO: strinol...;oo Inu co moy to:lo.TextRelayServiceTest+0.A SF fiminny@localhost)Á console (STAGING)D6.000ANo a-created at> DAT: SUBCMOWOL INTERVAL 3ODAYOIGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER RY sns count783704ДЕДЕЦЕА-215716select * fron teans where id = 1SELECTCONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN • ouner' ELSE ** END) AS user idsa.*t.ouner id FROM social accounts saconsole (PROD) x C Service.phpds consoe leu,Do jminny v8458185Y0 ACachado Codo XKick of a new oroisct. Make chunoeE732—73s— 734- 7357391I11MARAN A GAAR MAVE AOTONG WHAA AMAENA NE EROAOEO1Eselect * fron usersPARAtA GAAR MANGURAANOid IN ((18688. 13934. 7160):Sid = 7168 order by id desc Linit 10:select * fron teans where id = 1117select * fron activity searches where user_1d = 31954:Pohettendactivity search_filters where activity_search._id TN (88882. 88992)c. Fix Texikelay service TesteC Multi-Picklist Layout DisplaNtretnheet dor10"Pode swioth1 Wioderd Tesms 108:20 /5 charsl UTE-я*4 space...
|
85223
|
NULL
|
NULL
|
NULL
|
|
85223
|
2919
|
68
|
2026-05-28T12:13:07.239053+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970387239_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
SIUCKVIChWindowHelpPwovscors#12121 on JY-20963-fol SIUCKVIChWindowHelpPwovscors#12121 on JY-20963-folProinet vO SAvice pinhe heloers phitpelereobiecistraiconSamiceTesuphr©ActivityControlier.php© VoiceConsentPrefix.phpNotificationsnObservers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phpGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc(in Policie:aProviders• Queuea Reoositonea RulesaServicesActivitylealendarI Conferenceimh Rullhomen CopperMmermobiacte@ DecorateActivityDummyHelpers@ Hubspo(i) AccountSyncStrategyMAetioneMcontndSuncstrateon• D DTO› FieldsOLeAkоkyscrco.on© UsersHistory.phpС) Team.prpOneindi.onoC TextRelayServiceTest.phgMWiaKorE .env.localcass woeh excnos daseuoehraolcathis nuesconcelehehearOpportunivsvncstratcov- Pac nation-ProsoecSearchStratco→- RedisService traits• UtilsWechookc) Batchsunceo ector cho© BatchSvncRedisService.ofCleantonoCLeAAA Den CTadAcCenoe* Srecurn n the result of the Apicoulonivate function execurerequest cal aole saosvaitaiScacheKey = Sthis->getRateLinitcacheKeyO:ScachedExpiresAt = Redis::get(ScacheKey)if is_string(ScachedExpiresAt) dd is_nuneric(ScachedExpiresat))Srenaining = max( value: 1,:Gint) ScachedExpiresAt - timeOD:throw new RateLimitExceptiondmessage: "Hubspot rate Linit (cached circuit-breaker)'.tryrecurn sapzealle} catch (Throwable Se) $aanses shuoscorkare btI/ NX: only the firstlCwlansours seesne keyuoseouen 4snbunst. leave the TTl untouched so the1 wandow is not reset byrrent job hitting the limit.Redhis:iserScachekey.stoing careo + Scetry.fter)lstions: "EX" SretevAfter. "Nx")Sthis->100->warning('(Hubsoot] Recefved 429 fron APT". [contasd=> Sthis->config->get.dO.= Seosaetwesehaeiouthoow now Catolsetycontoolt mattior "Wnhennt notuinnod Wol Chotnultton Col:throw Seclnaalise rccarica ono118private function getRateLinitCacheKey: stringi...=custom.logA console (STAGING)-782703704785785789=718711-717722725727729—730737— 738—7491IIITX: AUTONAND a.created_at › DATE_:GROUP BY u.id, u.email,ORDER BY sns_count DESC;select + Eron teans, wherlselect + Eron coles,SELECTCONCATU.Id, CASE WHsa.*t.ouner id FROM sociJOIN users u on u.id = sJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM actávitioSELECT * FROM actávitioSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenselect * fron playbooksseleetron miawhonkeselect * fron playbook_ctseleet ron eoeselect * fron crn_field.SELECT * FROM crn_field_AOTM con Brelds 4 0M 4• JOIN activities a ON $NHERF Actvity "d = 7002ANn & con nrovsden 3alSELECT * FROM activity.mselect * fron text relayselect * fron activitiesselect * fron users whenPARAtT GAAR MANGUREANOselect & fron teans wherHoeActivity• Thu 28 May 15:13:06JiminnY …# backend# buescontusion-dinig# donut_timeengineerne# general# hapoy birthdavA infocoe intemal all# infra-chansed# infrastructure de#bu-team-infoiliminnv-bo# platform-team# platform-ticketstoroduct launche# random# released# suppor# thank-youssie the neoole of fimill6? Direct message.. Stefka Stoyanova1a llivana Netsevachuan omauPetko KashinskiGola Nimitrous: Todor Stamatol1 Stelivan Georsie.&r MireNikolav Vanko&. James GrahamLukas Kovalk y#:AppsS lin Gloud@ Describe what you are looking forVasil VasilevAdd canvas7 File2. Supervisor Shutdown Deadlock: In reapChildren®, callins pontl_waitpiilooayt the WNOHANG flag makes it a blockine call. If one child process gets stuck (c.g.waiting on a network timeout), the parent supervisor will freeze indehnitely and completely ignore your 10-second graceful shutdown lozic. (edited)3. Non-Atomic Redis Locks: The new ReindexTargetStatsReporter checks the lock with get and then creates it with set . Because the workers are truly parallel, they carboth read null at the exact same microsecond and both push duplicate metrics to Datados. This needs to be a sinale, atomic command (e.?. set with NX)).Vasil Vasilev 2:56 PM1 е добър коментарe intentonaККСНО ФлеелкОлИЕКОФИИИ ОИІКЕЛКУС ОНОК-ФТЬКОТУ-ЕЛТЬе ВОТАБОЖЕ БУЙ ССОХСТАХ ГОНОЯ МОНОМНИ ТОДОВИ АЙТСЯТЛУСЬНІ СЛЕ-ОЛФОНИМИКЯ СЕОХОЛЕLukas Kovalik 3.07 PMза трстата точка обаче наистина ли няма польожк!аз май си ги ползвам някьдеили е друг driverVasil Vasilev 3:10 PMне става, бях го направил така, и се счупиtkac Kowaiik 2utniomами и при мен се чумешеVasil Vasilev 3:11 PMoredrs nonnsoжa setSkoy, Svalue.lIinx = tmie. ex =0.0))Connrodis he craReLukas Kovalik 3:11 pNтака май не работеше Sresult = Redis-set(StestKey, StestValue, 'SX. StI. "NX"): а така ла Sresult = Redisuset(StestKey StestValue I'nx' 'ex' => Stll)Vasil Vasiley 3411 PvLukas Kovalik 3:12 PMне помня вечіVasil Vasilev 3:12 PMобратното може да сtkse Kowsll4 242.o1/може и да бръкамVacl Vacllev 2.12 pvRkOna uMаSlockAcquired - Redis::set(self::POLLING_LOCK.KEY, ison_encode(SlockDota), "EX", self::LOCK_TTL_SECONDS, "NX")ЗНАчИ ПЬОКОТО ЗЛБОТИMessage Vasil Vasiler...
|
NULL
|
1553543457553793673
|
NULL
|
click
|
ocr
|
NULL
|
SIUCKVIChWindowHelpPwovscors#12121 on JY-20963-fol SIUCKVIChWindowHelpPwovscors#12121 on JY-20963-folProinet vO SAvice pinhe heloers phitpelereobiecistraiconSamiceTesuphr©ActivityControlier.php© VoiceConsentPrefix.phpNotificationsnObservers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phpGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc(in Policie:aProviders• Queuea Reoositonea RulesaServicesActivitylealendarI Conferenceimh Rullhomen CopperMmermobiacte@ DecorateActivityDummyHelpers@ Hubspo(i) AccountSyncStrategyMAetioneMcontndSuncstrateon• D DTO› FieldsOLeAkоkyscrco.on© UsersHistory.phpС) Team.prpOneindi.onoC TextRelayServiceTest.phgMWiaKorE .env.localcass woeh excnos daseuoehraolcathis nuesconcelehehearOpportunivsvncstratcov- Pac nation-ProsoecSearchStratco→- RedisService traits• UtilsWechookc) Batchsunceo ector cho© BatchSvncRedisService.ofCleantonoCLeAAA Den CTadAcCenoe* Srecurn n the result of the Apicoulonivate function execurerequest cal aole saosvaitaiScacheKey = Sthis->getRateLinitcacheKeyO:ScachedExpiresAt = Redis::get(ScacheKey)if is_string(ScachedExpiresAt) dd is_nuneric(ScachedExpiresat))Srenaining = max( value: 1,:Gint) ScachedExpiresAt - timeOD:throw new RateLimitExceptiondmessage: "Hubspot rate Linit (cached circuit-breaker)'.tryrecurn sapzealle} catch (Throwable Se) $aanses shuoscorkare btI/ NX: only the firstlCwlansours seesne keyuoseouen 4snbunst. leave the TTl untouched so the1 wandow is not reset byrrent job hitting the limit.Redhis:iserScachekey.stoing careo + Scetry.fter)lstions: "EX" SretevAfter. "Nx")Sthis->100->warning('(Hubsoot] Recefved 429 fron APT". [contasd=> Sthis->config->get.dO.= Seosaetwesehaeiouthoow now Catolsetycontoolt mattior "Wnhennt notuinnod Wol Chotnultton Col:throw Seclnaalise rccarica ono118private function getRateLinitCacheKey: stringi...=custom.logA console (STAGING)-782703704785785789=718711-717722725727729—730737— 738—7491IIITX: AUTONAND a.created_at › DATE_:GROUP BY u.id, u.email,ORDER BY sns_count DESC;select + Eron teans, wherlselect + Eron coles,SELECTCONCATU.Id, CASE WHsa.*t.ouner id FROM sociJOIN users u on u.id = sJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM actávitioSELECT * FROM actávitioSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenselect * fron playbooksseleetron miawhonkeselect * fron playbook_ctseleet ron eoeselect * fron crn_field.SELECT * FROM crn_field_AOTM con Brelds 4 0M 4• JOIN activities a ON $NHERF Actvity "d = 7002ANn & con nrovsden 3alSELECT * FROM activity.mselect * fron text relayselect * fron activitiesselect * fron users whenPARAtT GAAR MANGUREANOselect & fron teans wherHoeActivity• Thu 28 May 15:13:06JiminnY …# backend# buescontusion-dinig# donut_timeengineerne# general# hapoy birthdavA infocoe intemal all# infra-chansed# infrastructure de#bu-team-infoiliminnv-bo# platform-team# platform-ticketstoroduct launche# random# released# suppor# thank-youssie the neoole of fimill6? Direct message.. Stefka Stoyanova1a llivana Netsevachuan omauPetko KashinskiGola Nimitrous: Todor Stamatol1 Stelivan Georsie.&r MireNikolav Vanko&. James GrahamLukas Kovalk y#:AppsS lin Gloud@ Describe what you are looking forVasil VasilevAdd canvas7 File2. Supervisor Shutdown Deadlock: In reapChildren®, callins pontl_waitpiilooayt the WNOHANG flag makes it a blockine call. If one child process gets stuck (c.g.waiting on a network timeout), the parent supervisor will freeze indehnitely and completely ignore your 10-second graceful shutdown lozic. (edited)3. Non-Atomic Redis Locks: The new ReindexTargetStatsReporter checks the lock with get and then creates it with set . Because the workers are truly parallel, they carboth read null at the exact same microsecond and both push duplicate metrics to Datados. This needs to be a sinale, atomic command (e.?. set with NX)).Vasil Vasilev 2:56 PM1 е добър коментарe intentonaККСНО ФлеелкОлИЕКОФИИИ ОИІКЕЛКУС ОНОК-ФТЬКОТУ-ЕЛТЬе ВОТАБОЖЕ БУЙ ССОХСТАХ ГОНОЯ МОНОМНИ ТОДОВИ АЙТСЯТЛУСЬНІ СЛЕ-ОЛФОНИМИКЯ СЕОХОЛЕLukas Kovalik 3.07 PMза трстата точка обаче наистина ли няма польожк!аз май си ги ползвам някьдеили е друг driverVasil Vasilev 3:10 PMне става, бях го направил така, и се счупиtkac Kowaiik 2utniomами и при мен се чумешеVasil Vasilev 3:11 PMoredrs nonnsoжa setSkoy, Svalue.lIinx = tmie. ex =0.0))Connrodis he craReLukas Kovalik 3:11 pNтака май не работеше Sresult = Redis-set(StestKey, StestValue, 'SX. StI. "NX"): а така ла Sresult = Redisuset(StestKey StestValue I'nx' 'ex' => Stll)Vasil Vasiley 3411 PvLukas Kovalik 3:12 PMне помня вечіVasil Vasilev 3:12 PMобратното може да сtkse Kowsll4 242.o1/може и да бръкамVacl Vacllev 2.12 pvRkOna uMаSlockAcquired - Redis::set(self::POLLING_LOCK.KEY, ison_encode(SlockDota), "EX", self::LOCK_TTL_SECONDS, "NX")ЗНАчИ ПЬОКОТО ЗЛБОТИMessage Vasil Vasiler...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85220
|
2919
|
66
|
2026-05-28T12:12:55.054305+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970375054_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fix rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fix-InWindow© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.php© ActivityObserver.php© ContactObserver.php© [EMAIL]© ProfileObserver.php© SocialAccountObserver.phpUserObserver.phpUserRoleObserver.php> Co PoliciesaProvidersD Queuea ReoositoneE RulesaServicesActivityAiReodrleallendar@ BullhornCopper|mermobiacteDecorateActivityDummy› @ Helpers~ HubspotAccountSyncStrategyMAetione( ContactSyncStrategywnapers.ohOLekоkyseiwico.onOAeindi.oneMWiaKorCass wochexeR82* Ecetucn T The result of the API calzprivate function executeRequest(callable SapiCall)ScacheKey = $this->getRateLinitCacheKeyO:scacheotxozresac = keots.toet scachekey1f (18_8tring(ScachedExpiresAt) S& 1s_numeric(ScachedExpiresAt)) €Srenaznzno = max value(int) ScachedExpiresAt - time()):throw new RateLimitExceptionCmessage:Hubspot rare biazx cached carcuz-breaken"return SapiCallO:} catch (Throwable Se)fif (Sthis->isHubspotRateLinit(Se)) (Srerryaten"Thisornarse?erny.tTer e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 429s in the sane burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the limit.Redis: :set(ScacheKey, (string) (tine() + SretryAfter),Sthis->log->warning(^[Hubspot] Received 429 fron API', [› Fields'team id'=> Sthis->config->tean id.# Journal'config_id'= Sthis->config->getIdO.Ea Metadata'retry_after' => SretryAfter,=> Se->getMessageo.Opportunivsvncstratcov- Pac nation-ProsoecSearchStratco→- Redisthrow new RateLinitException( message: 'Hubspot returned 429', SretryAfter, $e);Service traits• Utilsthrow Ser- Wechookc) Batchsunceo ector cho© BatchSyncRedisService.ptCleantono© ClosedDealStagesService.clnaalise rccarica onoprivate function getRateLimitCachekey(): stringf...o Inu co moy 1o:l2o4U TextRelayServiceTest~+0.A SF (iminny@localhost)console [PROD] x © Service.php# console fiaumA console [STAGING)D000€ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYIGROUP BY u.id, v.enail, U.nane, u.softphone_numberORDER BY sns_count-782783704ВАДДЕВЕК715716FRRREE728729E732—73s-734-736737- 73568fminry~045 A1 441 X 66 Aselect * fron teans where id = 1;SELECTCONCAT(u.1d, CASE WHEN U.id = t.ouner_id THEN • (ouner)' ELSE "* END) AS user_id,sa.*,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 = 1117 and sa.provider = 'hubspot":SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YESSELECT * FROM activitios WHERE Uuid_to_bin('[CREDIT_CARD]-927f-4f4da208185c*) = uuid; # 80186192 NOid = 5473;select * fron playbook_categories where id = 43783;lect * fron playbook_categories where playbook_id = 5473;select * fron crn_fields where id = 659242)crn_field_values where crn_field_ id = 659242SELECT * FROM crm_fleld_data fd* JOIN crn_fields fON fd.crm_field_id = f.id• JOIN activities a ON fd.activity_id = a.10WHERE activity_id = 79933459• AND f.crn_provider_id = 'hs_activity_type';select * fron text_relays where created_at > '2026-85-01':select * fron activities where user id IN (7168.select * fron users where team_id = 1 and id IN (18608, 13934, 7160);select * fron activities where user_id = 7160 order by id desc linit 10;select * fron teans where id = 1117;select x fron activity_searches where user_id = 31054;select + fronactivity_search_filters where activity_search_id IN (88882, 88902);Cachado Codo XoKick off a new project. Make changeseEnexthal Sanha yC Multi-Picklist Layout DisplaeClient.phpfL99-182"PodswiothW Wodtur Teams 10247 UTE-яensd....
|
NULL
|
5171856020865434183
|
NULL
|
click
|
ocr
|
NULL
|
rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fix rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fix-InWindow© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.php© ActivityObserver.php© ContactObserver.php© [EMAIL]© ProfileObserver.php© SocialAccountObserver.phpUserObserver.phpUserRoleObserver.php> Co PoliciesaProvidersD Queuea ReoositoneE RulesaServicesActivityAiReodrleallendar@ BullhornCopper|mermobiacteDecorateActivityDummy› @ Helpers~ HubspotAccountSyncStrategyMAetione( ContactSyncStrategywnapers.ohOLekоkyseiwico.onOAeindi.oneMWiaKorCass wochexeR82* Ecetucn T The result of the API calzprivate function executeRequest(callable SapiCall)ScacheKey = $this->getRateLinitCacheKeyO:scacheotxozresac = keots.toet scachekey1f (18_8tring(ScachedExpiresAt) S& 1s_numeric(ScachedExpiresAt)) €Srenaznzno = max value(int) ScachedExpiresAt - time()):throw new RateLimitExceptionCmessage:Hubspot rare biazx cached carcuz-breaken"return SapiCallO:} catch (Throwable Se)fif (Sthis->isHubspotRateLinit(Se)) (Srerryaten"Thisornarse?erny.tTer e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 429s in the sane burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the limit.Redis: :set(ScacheKey, (string) (tine() + SretryAfter),Sthis->log->warning(^[Hubspot] Received 429 fron API', [› Fields'team id'=> Sthis->config->tean id.# Journal'config_id'= Sthis->config->getIdO.Ea Metadata'retry_after' => SretryAfter,=> Se->getMessageo.Opportunivsvncstratcov- Pac nation-ProsoecSearchStratco→- Redisthrow new RateLinitException( message: 'Hubspot returned 429', SretryAfter, $e);Service traits• Utilsthrow Ser- Wechookc) Batchsunceo ector cho© BatchSyncRedisService.ptCleantono© ClosedDealStagesService.clnaalise rccarica onoprivate function getRateLimitCachekey(): stringf...o Inu co moy 1o:l2o4U TextRelayServiceTest~+0.A SF (iminny@localhost)console [PROD] x © Service.php# console fiaumA console [STAGING)D000€ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYIGROUP BY u.id, v.enail, U.nane, u.softphone_numberORDER BY sns_count-782783704ВАДДЕВЕК715716FRRREE728729E732—73s-734-736737- 73568fminry~045 A1 441 X 66 Aselect * fron teans where id = 1;SELECTCONCAT(u.1d, CASE WHEN U.id = t.ouner_id THEN • (ouner)' ELSE "* END) AS user_id,sa.*,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 = 1117 and sa.provider = 'hubspot":SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YESSELECT * FROM activitios WHERE Uuid_to_bin('[CREDIT_CARD]-927f-4f4da208185c*) = uuid; # 80186192 NOid = 5473;select * fron playbook_categories where id = 43783;lect * fron playbook_categories where playbook_id = 5473;select * fron crn_fields where id = 659242)crn_field_values where crn_field_ id = 659242SELECT * FROM crm_fleld_data fd* JOIN crn_fields fON fd.crm_field_id = f.id• JOIN activities a ON fd.activity_id = a.10WHERE activity_id = 79933459• AND f.crn_provider_id = 'hs_activity_type';select * fron text_relays where created_at > '2026-85-01':select * fron activities where user id IN (7168.select * fron users where team_id = 1 and id IN (18608, 13934, 7160);select * fron activities where user_id = 7160 order by id desc linit 10;select * fron teans where id = 1117;select x fron activity_searches where user_id = 31054;select + fronactivity_search_filters where activity_search_id IN (88882, 88902);Cachado Codo XoKick off a new project. Make changeseEnexthal Sanha yC Multi-Picklist Layout DisplaeClient.phpfL99-182"PodswiothW Wodtur Teams 10247 UTE-яensd....
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85219
|
2918
|
62
|
2026-05-28T12:12:54.950020+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970374950_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes...
|
[{"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":"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}]...
|
-5491484594888514086
|
-7050964252790406720
|
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
IDE and Project Settings
Sync Changes
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"‹$0(ah]100% <78• Thu 28 May 15:12:54181ec2-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
|
NULL
|
NULL
|
NULL
|
|
85218
|
2918
|
61
|
2026-05-28T12:12:50.898182+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970370898_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3...
|
[{"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":"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,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"}]...
|
-45081608812797713
|
-8776120261847823424
|
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
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O &4‹$0(ah]A100% <78 • Thu 28 May 15:12: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...
|
85216
|
NULL
|
NULL
|
NULL
|
|
85217
|
2919
|
65
|
2026-05-28T12:12:50.790886+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970370790_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.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...
|
[{"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}]...
|
6423983795013307969
|
-3852565242246887264
|
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
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lproidetwnapers.oh© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.pho© ProfileObserver.phgC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc> (i PoliciesuProvider• Queuea Reoositonea RulesaServicesDa ActivityAiReodnleallendarIiconnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyMAetioneMcontndSuncstrateon• D DTO> FieldsThtauendOLekоkyseiwico.on© Kernel.phgMWiaKorCass wochexeR82* Breturn T The result of the APi callOpportunivsvncstratcov> ea Pacination-ProsoecSearchStratcoprivate function executeRequest(callable SapiCall)ScacheKey = Sthis->getRateLimitCacheKeyO:scachedcxpzresar = keozs..cetscachekey)if as string(ScachedExpiresAt) 8& is nuneric(ScachedExpiresat)Srenainznd = aax var(int) ScachedExpiresAt - tineOn:throw new RateLinitExceptionnessase: Huospot rate uinzx cached circuz-breaker")Srenaznzno,retuen Santmaiero"carch aihcomable Sei6 (SthisosisHubsnotPateLinit(se)) €Sretry.terihisornarse?erny.ter eNx: only the Siirst doh to racesve a 420 sin this hunst sets the kev.ame burst leave the TTL untouched so theExtract Surround: , concurrent job hitting the linit.Redhis..set/Scachekeylistoindl trelll 4 SoetnultterSthis->Log->warning(*[Hubspot) Received 429 from API". ["tean fa= Sthssosconttaosteae5alconfig_id'=> Sthis->config->getido'retry after' => SretryAfter.-CWAeMANAAAY→- Redisthrow new RateLimitException( message: "Hubspot returned 429' SretcyAfter. Se):Service traits• Utilsthrow ser- Wechookc) Batchsunceo ector cho© BatchSvncRedsService.oCleantono@ GiosedDealStadpsSeniceclnaalise rccarica onopnivate functiion getRateLimitCachekev@: stoinof.a Moul canhar lanauaoas. 19 minutes.a00)o Inu co moy 1o:l2ro0TextRelayServiceTest+0.A SF fiminny@localhost)A console (STAGING)D6.00€ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYITGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER RY sns count783704RRRBERE4714715716select * fron teans where id = 1SELECTCONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN " ouner' ELSE ** END) AS usen idt.ouner id FROM social accounts saJOIN users u on u.id = sa.sociable icJOIN teans t1.n<->1: on t.id = u.tean idNHERE u.tean_id = 1117 and sa.provider = *hubspot"console (PROD) x C Service.phpds consoeleu.Dojminnyv8458185Y0 ACachado Codo XoKick of a new proisct. Make chunoe726727E732—730- 735SEIFM+ EROM eo Geld data&I0TM con Srelds 4 0M &d.oom Seid 3ds€.5%•I0TM actvs tes a 0M &dlactvsty sdea.aWHERE activity_id = 79933459• AND f.crn_provider_id = 'hs_activity_type';MARAt" GAAR NANRURASNe WhNAA MONA SA TM CRAROselect * fron users where team id = 1 and id IN (18688. 13934. 7160):MARAtGAAs NANRURAANenid = 7168 order by id desc Linit 10:ohettene neaee whond noeddiue lecnheotte m24004449select * fron teans where id = 1117select * fron activity searches where user_1d = 31054:Pohettendactivity search_filters where activity_search._id TN (88882. 88992)c. Fix Texikelay service TesteC Multi-Picklist Layout DisplaNtietnheetdor10"Podswiothwwodtretee100-97 197 chars. 1 Foe braskl UTE-я*4 space...
|
85215
|
NULL
|
NULL
|
NULL
|
|
85216
|
2918
|
60
|
2026-05-28T12:12:44.309611+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970364309_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.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'...
|
[{"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}]...
|
-1678849104122188672
|
593894980799643872
|
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'
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O &4‹$0(ah]A100% <78• Thu 28 May 15:12:43181ec2-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
|
NULL
|
NULL
|
NULL
|
|
85215
|
2919
|
64
|
2026-05-28T12:12:43.840655+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970363840_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.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...
|
[{"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}]...
|
1962564511686315741
|
1735522336325714144
|
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
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-l© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phpC ActivityObserver.pho© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.php© SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc(in Policie:uProvider• Queueu Reoositoniea RulesaServicesDa ActivityAiReodrConnerMmermobiacte@ DecorateActivityDummym Helnarsv m Hibsnd(i) AccountSyncStrategyIhctioneMcontndSuncstrateon• D DTO> FieldsThtauendwnapers.ohOLeAkоkyscrco.on182Opportunivsvncstratcov- Pac nation-ProsoecSearchStratco→- RedisService traits• Utils- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCleantono@ GiosedDealStanpsSenice© Kernel.phgMWiaKorClientrucn T The result of the APi calltunatan oyomitoponuoctenllahlo ConscaliIchthty - sumsopeckoteonzcudehcheyicheotxozresar = keozs..oecscachekeyas string(ScachedExpiresAt)) 8& is numeric(ScachedExpiresaD))Sremaznzno = nax value.ano scacheotxozresar . cnelothrow new RateLinitExceptiondSremdiningrerumSaniwaiaosarch ethrowabie Se14 (Sthiso>isHubsnotRateLinit(Se))metrvittenthisornarse?etry.tter e"/ NX: only the first job to receive a 429 in this burst sets the key// Subsequent 42%s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the Uinit.Redis::set(ScacheKey, (string) (time®) + SretryAfter)."Sy" Snetau ftenExtractSurroundStnis->L0q->warnznge THuDspol Received 429 from API'.SthisoscontacsteanalCascade 22171configid'=> Sthis->config->getido,'retry after" => SretryAfter=> Se->getMessageOthrow new RateLinitException( message: "Hubspot returned 429". SretryAfter. Se)throw Senclnaalise rccarica ono> function getRateLinitCachekevO: stringf...o Inu co moy to:l2.4sTextRelayServiceTest+0.A SF fiminny@localhost)console (STAGING)D6.00€ANo a-created at > DAT: SUBCNOWOL VINTERVAL XA DAYIIGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER RY sns count7837048 2.ДRЕВЕДАВ715716RRARNRRCommand %727313!E732— 734- 735select * fron teans where id = 1SELECTCONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN " ouner' ELSE ** END) AS user idsa.*t.ouner id FROM social accounts saSEIFM + EROM eo Geld data&I0TM con Srelds 4 0M &d.oom Seid 3ds€.5%IOTM actvs tres a 0M &dlactvsty sdsa.aTHERF Actvity 3d= 79073459•AND & eon nrovsden 3dl= the actausty tune*select * fron text_relays where created_at > ^2826-05-01':MARAt"GAAR NANGURASNS WhNA NONn SA TM CRAROselect * fron users where team id = 1 and id IN ((18688. 13934. 7160):select * fron activities where user id = 7168 order by id desc Linit 10:ohettene neaed whond coedpue locnhootl. m24004449select * fron teans where id = 1117select * fron activity searches where user_1d = 31954:Pohettendactivity search_filters where activity_search._id TN (88882. 88992)d console (PROD) x C Service.phpds consoe leu,Dojminnyv8458185Y0 ACacaado CodoxoKick of a new oroiset. Make chunoseEnexthal Sanha yC Multi-Picklist Layout DisplaNtietnheetdor10"Podswiothanausoss. 19 minutes.a00WiederTanet Ceetiktkohnreon braske hiehensd....
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85214
|
2919
|
63
|
2026-05-28T12:12:41.738477+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970361738_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-209 rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-l© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phpC ActivityObserver.pho© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.pho(in Policie:aProviders• Queueu Reoositoniea RulesaServicesDa ActivityAiReodrConnerimermobiacte@ DecorateActivityDummym Helnarsv m Hibsnd(i) AccountSyncStrategyIhctioneMcontndSuncstrateon• D DTO> FieldsThtauendwnapers.oh=custom.loglaravel.logA SF fiminny@localhost)HS Jocal (jiminny(blocalhost)d console (PROD) x C Service.php# console fiaumOLekоkyseiwico.on182Opportunivsvncstratcov- Pac nation-ProsoecSearchStratco→- RedisService traits• UtilsWechookc) Batchsunceo ector cho© BatchSvncRedisService.o+eantond@ GiosedDealStanesSenice© Kernel.phgC Client.php Xphp autoload.phpMWiaKorClient extendsA console (STAGING)00 €ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYITGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER BY sns_count DESC;Dojminnyv045 A1 A41 У 66 Arucn T The result of the APi call-782703704select * fron teans where id = 1tunatan oyomitoponuoctenllahlo ConscaliicheKey = Sthis->getRateLimitCacheKeyOicheotxozresar = keozs..oecscachekeyas string(ScachedExpiresAt)) 8& is numeric(ScachedExpiresaD))Sremaznzno = nax value.ano scacheotxozresar . cneloRRERERESselect + Eron coles.CEIEOTCONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN • ouner' ELSE ** END) AS usen idthrow new RateLinitExceptiondSremdiningrerumSaniwaiaosarch ethrowabie Se14 (Sthiso>isHubsnotRateLinit(Se))TTIITHmetrvittenthisornarse?etry.tter e"725sa.*t.ouner id FROM social accounts sJOIN users u on u.id = sa.sociable icJOIN teans t1.n<->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot' :SELECT * FROM activitios WHERE uuid to_bin('8024fffb-2df7-4817-91f4-d9f896050248') = uuid: # 79933459 YESSELECT * FROM actávitios WHERE uuid to_bin(*[CREDIT_CARD]-927f-4f4da2a8185c') = uuid: # 80186192 NOSELECT * FROM crn configurations WHERE id = 1853SELECT * FROM teans WHERE id = 1117;sellee ron usens where oe 387491select * fron playbooks where id = 5473;select + fron plavbook categonies nhere id = 43787:select * fron playbook_categories where playbook_id = 5473;selleet ron emeds whene 0m 65977select + fron con Sield values nhere con Sield jd n 659942/I NX: only the first job to receive a 429 in this burst sets the key,// Subsequent 429s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the limit.Redis::set(ScacheKey, (string) (time®) + SretryAfter)ExtractSurroundStnis->L0q->warnzng@ THuDspol Received 429 from API'."taanSd= Sthfsoscont aostean alCascade y171Command %SELECT * FROM crn_field_data foI0TM ceon Srelds 4 0M &d.oom Seid 3ds€.30IOTM actvs tres a 0M &dlactavsty dea.anMethod call is orovided S, parameters. but the method sianature uses 3 parameter.C Reditconfigid'=> Sthis->config->getido,'retry after" => SretryAfter=> Se->getMessageOamethod set(string $key, Svalue, Soptions = null):Redis Illuminate Support \Facades stringlIlluminate Support FacadesboolParameterse stafnal Skeyby id descthrow new RateLinitException( message: "Hubspot returned 429'. SretryAfter, Se)Sen aisonthrow SenSourcelendo.srawelitamework/src//lluminate/suonor/Facades/Redts.ondselect * fron teans where id = 1117select * fron activity searches where user_1d = 31954:select * Fronactivity search_filters where actiivity_search_id TN (88882. 88992)clnaalise rccarica ono> function getRateLinitCachekevO: stringf...• • Thu 28 May 15:12:41TextRelayServiceTest+0.Cacaado CodoxoKick off a new proinet. Make chanod.Acroce wour Antird codahaedC, Fix TextRelayService TestsC Multi-Picklist Layout DisplaNtietnheetdor10Code swr.adas for othar lanauaoas. 19 minutes.ao0Windeladent00-47 RRR chars.3 Fon braskel UTE-яaudenon....
|
NULL
|
7865224694538065099
|
NULL
|
click
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-209 rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-l© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phpC ActivityObserver.pho© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.pho(in Policie:aProviders• Queueu Reoositoniea RulesaServicesDa ActivityAiReodrConnerimermobiacte@ DecorateActivityDummym Helnarsv m Hibsnd(i) AccountSyncStrategyIhctioneMcontndSuncstrateon• D DTO> FieldsThtauendwnapers.oh=custom.loglaravel.logA SF fiminny@localhost)HS Jocal (jiminny(blocalhost)d console (PROD) x C Service.php# console fiaumOLekоkyseiwico.on182Opportunivsvncstratcov- Pac nation-ProsoecSearchStratco→- RedisService traits• UtilsWechookc) Batchsunceo ector cho© BatchSvncRedisService.o+eantond@ GiosedDealStanesSenice© Kernel.phgC Client.php Xphp autoload.phpMWiaKorClient extendsA console (STAGING)00 €ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYITGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER BY sns_count DESC;Dojminnyv045 A1 A41 У 66 Arucn T The result of the APi call-782703704select * fron teans where id = 1tunatan oyomitoponuoctenllahlo ConscaliicheKey = Sthis->getRateLimitCacheKeyOicheotxozresar = keozs..oecscachekeyas string(ScachedExpiresAt)) 8& is numeric(ScachedExpiresaD))Sremaznzno = nax value.ano scacheotxozresar . cneloRRERERESselect + Eron coles.CEIEOTCONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN • ouner' ELSE ** END) AS usen idthrow new RateLinitExceptiondSremdiningrerumSaniwaiaosarch ethrowabie Se14 (Sthiso>isHubsnotRateLinit(Se))TTIITHmetrvittenthisornarse?etry.tter e"725sa.*t.ouner id FROM social accounts sJOIN users u on u.id = sa.sociable icJOIN teans t1.n<->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot' :SELECT * FROM activitios WHERE uuid to_bin('8024fffb-2df7-4817-91f4-d9f896050248') = uuid: # 79933459 YESSELECT * FROM actávitios WHERE uuid to_bin(*[CREDIT_CARD]-927f-4f4da2a8185c') = uuid: # 80186192 NOSELECT * FROM crn configurations WHERE id = 1853SELECT * FROM teans WHERE id = 1117;sellee ron usens where oe 387491select * fron playbooks where id = 5473;select + fron plavbook categonies nhere id = 43787:select * fron playbook_categories where playbook_id = 5473;selleet ron emeds whene 0m 65977select + fron con Sield values nhere con Sield jd n 659942/I NX: only the first job to receive a 429 in this burst sets the key,// Subsequent 429s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the limit.Redis::set(ScacheKey, (string) (time®) + SretryAfter)ExtractSurroundStnis->L0q->warnzng@ THuDspol Received 429 from API'."taanSd= Sthfsoscont aostean alCascade y171Command %SELECT * FROM crn_field_data foI0TM ceon Srelds 4 0M &d.oom Seid 3ds€.30IOTM actvs tres a 0M &dlactavsty dea.anMethod call is orovided S, parameters. but the method sianature uses 3 parameter.C Reditconfigid'=> Sthis->config->getido,'retry after" => SretryAfter=> Se->getMessageOamethod set(string $key, Svalue, Soptions = null):Redis Illuminate Support \Facades stringlIlluminate Support FacadesboolParameterse stafnal Skeyby id descthrow new RateLinitException( message: "Hubspot returned 429'. SretryAfter, Se)Sen aisonthrow SenSourcelendo.srawelitamework/src//lluminate/suonor/Facades/Redts.ondselect * fron teans where id = 1117select * fron activity searches where user_1d = 31954:select * Fronactivity search_filters where actiivity_search_id TN (88882. 88992)clnaalise rccarica ono> function getRateLinitCachekevO: stringf...• • Thu 28 May 15:12:41TextRelayServiceTest+0.Cacaado CodoxoKick off a new proinet. Make chanod.Acroce wour Antird codahaedC, Fix TextRelayService TestsC Multi-Picklist Layout DisplaNtietnheetdor10Code swr.adas for othar lanauaoas. 19 minutes.ao0Windeladent00-47 RRR chars.3 Fon braskel UTE-яaudenon....
|
85210
|
NULL
|
NULL
|
NULL
|
|
85213
|
2918
|
59
|
2026-05-28T12:12:41.615361+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970361615_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"84‹$0(ah]100% <78• Thu 28 May 15:12:41181ec2-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
|
4221631536197410693
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"84‹$0(ah]100% <78• Thu 28 May 15:12:41181ec2-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...
|
85212
|
NULL
|
NULL
|
NULL
|
|
85212
|
2918
|
58
|
2026-05-28T12:12:31.669999+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970351669_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results...
|
[{"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":"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,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
7274838338855959483
|
5225835645296838756
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85211
|
2918
|
57
|
2026-05-28T12:12:30.566629+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970350566_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
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 ...
|
[{"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":"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,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
-3264450658369633090
|
1136716483744153188
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
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 ...
|
85207
|
NULL
|
NULL
|
NULL
|
|
85210
|
2919
|
62
|
2026-05-28T12:12:30.464124+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970350464_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
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 ...
|
[{"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":"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.36801863,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"bounds":{"left":0.37799203,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","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;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
-3264450658369633090
|
1136716483744153188
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
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 ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85209
|
2919
|
61
|
2026-05-28T12:12:29.441339+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970349441_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
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":"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.36801863,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"bounds":{"left":0.37799203,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","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}]...
|
4398856711508997131
|
-8740131047306179648
|
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
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-lproidetwnapers.oh© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phpGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.php© UserObserver.pho© UserRoleObserver.phc> (i PoliciesuProvider• Queuea Reoositonea RulesaServicesDa ActivityAireoorsleallendarIiconnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyMAetioneD ContactSyncStrateay› DDTO> FieldshlaurndOLekоkyseiwico.on© Kernel.phgMWiaKorwochnexeR82* Breturn T The result of the APi callOpportunivsvncstratcov> Ma pacination-ProsoecSearchStratcoprivate function executeRequest(callable SapiCall)ScacheKey = Sthis->getRateLimitCacheKeyO=scachedcxpzresar = keozs..cetscachekey)if as string(ScachedExpiresAt) 8& is nuneric(ScachedExpiresat)Srenainznd = aax vau(int) ScachedExpiresAt - timeOn:throw new RateLinitExceptionnessase: Huospot rate uinzx cached circuz-breaker")Srenazningretuen Santmaiero"carch aihcomable Sei6 (SthisosisHubsnotPateLinit(se)) €Sretry.terihisornarse?erny.ter e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 429s in the sane burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redhis..set Scachekey "strinall Trabl4 Soetaulster• ExtractStnIs->L0g->warnIngCTHUOSpOT) Received 429 from API". (= Sthssosconttaosteae5al=> Sthis->config->getido'retry after' => SretryAfter.-CWAeMANAAAY→- Redisthrow new RateLimitException( message: "Hubspot returned 429'. SretcyAfter. Se):Service traits• Utilsthrow ser- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCleantone@ GiosedDealStaapsSeniceclnaalise rccarica onopnivate functiion getRateLimitCachekev@: stoinof.e tor othar lanauaoas. 19 minutes.a00)•• Thu 28 May 15:12:29TextRelayServiceTest+0.A SF fiminny@localhost)A console [STAGING)D6.00€ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYITGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER RY sns count795704BEBIEE714-215select * fron teans where id = 1CEIEOTCONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN " ouner' ELSE ** END) AS user idt.ouner id FROM social accounts saJOIN users u on u.id = sa.sociable icJOIN teans t1.n<->1: on t.id = u.team_idNHERE u.tean_id = 1117 and sa.provider = *hubspot"d console (PROD) x C Service.php# console fiaumDojminnyv045 A1 A41 У 66 ACachado Codo XoKick off a new proinet. Make chanod.726=732- 735SELECT * FROM Crn.field_data fd* JOIN crn_fields f ON fd.crm_field_id = f.1o* JOIN activities a ON fd.activity_id = a.1dWHERE activity_id = 79933459• AND f.crn_provider_id = 'hs_activity_type';select * fron text_ relays where created at > *2826-85-81*:MARAt" GAAR NANRURASNe WhNAA MONA SA TM CRAROselect * fron users where team id = 1 and id IN ((18688. 13934. 7160):select * fron activities where user id = 7168 order by id desc Linit 10.ohettene neaed whond coedpue locnhootl. m24004449select * fron teans where id = 1117select * fron activity searches where user_1d = 31954:Pohettendactivity search_filters where activity_search._id TN (88882. 88992)eEnexthal Sanha yC Multi-Picklist Layout Displantamahieuato Podeswiettwwoceueta100-07 132R chars.3 Foe braskel UTE-яensd....
|
85208
|
NULL
|
NULL
|
NULL
|
|
85208
|
2919
|
60
|
2026-05-28T12:12:27.347624+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970347347_m2.jpg...
|
PhpStorm
|
faVsco.js – JiminnyDebugCommand.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
SIUCKWindowHelpPwovscors#12121 on JY-20963-folProi SIUCKWindowHelpPwovscors#12121 on JY-20963-folProinet vhe bolbees.phetpelereobiecistraiconSamiceTesuphr©ActivityControlier.phpcliwarduscomnhotenLOAkоkyscwcoono TeamuprpOneindi.onoJiminhycocnee.corcom monoC TextRelayServiceTest.phgJiminhyoecrowoolor© JiminnyTokeninfoCommand.rE .enviloca1soackeac Jaainny console command.OMaxeobexaweeocinetC) ManageScimForTeam.choC) MarkBranchForEnvironmentPclass Jninnybebugcommand extends connanoc Propadatecoaching Feed oacc) Purgeconterences. onoC Removeunused Pario oentsoC Resetsastcsascch.onocRactorsCiamermotorancPineCeoeesolatacCaletitne noncCandNicossynittidaw(© SyncActivity.pho(© Trackimported.php@ MhichWarkoeleWadinadolM SchedulineC) KernelpheMGontracte› @b Domain> CODTO> ê Emails.› Ca Enums› Ea Evente› E Exceptions> O FFMoed0n Formatcen Helpers>M AcceseTokenProvidery controller•MApIMAlel ScornamAlteoonte338989889• Imsosline chteIOpportunn# PageIn CeMoAsthe-Cottinatprotected ssignature =1mnny:debug factzon?,"public function handle: voidi...private function testRedisset: vo1dhssosnfalt sinnor testand rects satruith phopeais suntay.StestKey = 'test:ratelinit:' . tine®:StestValue = (string) (tine() + 68):$ttl = 60;trySresult = Redis::set(StestKey. StestValue, 'EX'. Sttl, 'NX'):Oresult - Keoteoeciorestney prcatralue= SttuDAif (Sresult)CAkthAnGAe strina'7 Redis SET with PhoRedis syntax succeeded)CahteinGastein"Value: (StestValue)*):sthis-sintod"Tusirr secondsSretrtevedValue = Redis:"ger stestxev"Sthis->info( string: "Retrieved value: (SretcievedValuel"):Redis::del(Stestkey)Sthis-sinfodstVtest key deleted"else 1Sthis-serror( string: *X Rediis SET returned false (key nay already exist)*):carchult sxcenton Sanllped satateeSe->getHessageOD:thisesanmon t sthne. "Tinacaraosostiinaes corasodlo=custom.logtravelosA console (STAGING)7827051—780709733712713714717-729734736TX: AUTONAND a.created_at › DATE_:GROUP BY u.id, u.email,ORDER BY sns_count DESCselect + Eron teans, wherlselect + Eron coles,SELECTCONCATU.Id, CASE WHsa.*t.ouner id FROM sociJOIN users u on u.id = sJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM activitioSELECT * FROM actávitiosSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenselect * fron playbooksseleetron miawhonkeselect * fron playbook_ctseleet ron eoeselect * fron crn_field.vSELECT * FROM crn_field_AOTM con Brelds 4 0M 4• JOIN activities a ON $NHERF Actvity "d = 7002AND € con onovider saSELECT * FROM activity.mselect * fron text relayMARAtA GAAR MANGUSAENOselect * fron users whenPARAtT GAAR MANGUREANOPAlRetGhAn dAAAIRdAMselect * fron teans wherPhat"toon actytysselect * fron activitysHoeDM:ActivityMonJiminnY …# backend# buescontusion-dinig# donut_time#enpineerine# generall# happy birthdavA infocoe intemal all# infra-chansed# infrastructure de#bu-team-infoiliminnv-bost olatform-team# platform-ticketstoroduct launche# random# released# suppor# thank-youssie the neoole of fimill6? Direct message.Vasil VasilenF8. Stefka Stoyanova1a llivana NetsevaA Ctovan TomnulPetko KashinskiGola Nimitrous: Todor Stamator1 Stelivan Georsie.& MireNikolav Vanko&. James GrahamLukas Kovalky#:AppsG tia Gloudai Gooele Calel1O0% LXo Inu co woy to:lc@ Describe what you are looking for€. Vasil Vasileve Messageru Add canvasu riiesLukas Kovalik 2:45 PMНе знам това дали е релевантно1. Shared Elasticsearch Sockets: Because pontl_fork® clones file descriptors, both child processes will share the exact same HTTP/TCP socket for Elasticsearch. Sinceyou didnit reset the es client in resetAfterFork (like you did for Redis and DB), the parallel workers will collide at the network layer, causing corrupted data and cURLexcenhons2. Supervisor Shutdown Deadlock: In reapthildren@.calling pcntl_waitpid@ without the WNOHANG flag makes it a blocking call. It one child process gets stuck (e.g..waiting on a network timeout), the parent supervisor will treeze indehnitely and completely ignore your 10-second gracetul shutdown logic. (cdited)3. Non-Atomic Redis Locke. The new Reindexmetrocicorer ceneoe winl oet sinnencelestwinl ser.Weeuse the workeste ruvorellelnev cboth read null at the exact same microsecond and both puch duplicate metrics to Datados. This needs to be a sinele, atomic command (e.c. set with Nxà).Vasil Vasiley 2-56 PM•с ло0ьо комcнтар2 e intentionalЗ сьщо e intentional поради факта, че phpredis driver-а не поддьржа set ex +nх параметри, и трябва да се пуснат две операции set + expirmuse Kousll4 4.07.01/ок, аз го пуснах пак през geminза третата точка обаче наистина ли няма подържкаARMhnCH CH BOORRAM HAK:OGили е друг driverVasil Vasiley 2-10pwHe Gaaa.oay co MannaRиn TаКа,М GO СUМTMLukas Kovalik 3.10 pNVasil Vasiley 3411pvoears nonuooka set Kevalve.nxe tueexeouc phpredis не ставаLukas Kovalik 3-11 PMтака май не работеше Sresult = Redis-set(StestKey, StestValue, 'EX", Sttl, "NX"): а така na Sresult = Redisuset(StestKey, StestValue, 'nx, 'ex' => Stti).Vacil Vaciley aunowвторото пробвах, и не станаTukse Kouslly 2.44.01nuahnatuaне помня вридVasil Vasilev 3.12 PMOATTHOTO MONA NAALukas Kovalik 3412.pNможе и ли боькамMessage Vasil Vasiley...
|
NULL
|
-7274376422222260374
|
NULL
|
click
|
ocr
|
NULL
|
SIUCKWindowHelpPwovscors#12121 on JY-20963-folProi SIUCKWindowHelpPwovscors#12121 on JY-20963-folProinet vhe bolbees.phetpelereobiecistraiconSamiceTesuphr©ActivityControlier.phpcliwarduscomnhotenLOAkоkyscwcoono TeamuprpOneindi.onoJiminhycocnee.corcom monoC TextRelayServiceTest.phgJiminhyoecrowoolor© JiminnyTokeninfoCommand.rE .enviloca1soackeac Jaainny console command.OMaxeobexaweeocinetC) ManageScimForTeam.choC) MarkBranchForEnvironmentPclass Jninnybebugcommand extends connanoc Propadatecoaching Feed oacc) Purgeconterences. onoC Removeunused Pario oentsoC Resetsastcsascch.onocRactorsCiamermotorancPineCeoeesolatacCaletitne noncCandNicossynittidaw(© SyncActivity.pho(© Trackimported.php@ MhichWarkoeleWadinadolM SchedulineC) KernelpheMGontracte› @b Domain> CODTO> ê Emails.› Ca Enums› Ea Evente› E Exceptions> O FFMoed0n Formatcen Helpers>M AcceseTokenProvidery controller•MApIMAlel ScornamAlteoonte338989889• Imsosline chteIOpportunn# PageIn CeMoAsthe-Cottinatprotected ssignature =1mnny:debug factzon?,"public function handle: voidi...private function testRedisset: vo1dhssosnfalt sinnor testand rects satruith phopeais suntay.StestKey = 'test:ratelinit:' . tine®:StestValue = (string) (tine() + 68):$ttl = 60;trySresult = Redis::set(StestKey. StestValue, 'EX'. Sttl, 'NX'):Oresult - Keoteoeciorestney prcatralue= SttuDAif (Sresult)CAkthAnGAe strina'7 Redis SET with PhoRedis syntax succeeded)CahteinGastein"Value: (StestValue)*):sthis-sintod"Tusirr secondsSretrtevedValue = Redis:"ger stestxev"Sthis->info( string: "Retrieved value: (SretcievedValuel"):Redis::del(Stestkey)Sthis-sinfodstVtest key deleted"else 1Sthis-serror( string: *X Rediis SET returned false (key nay already exist)*):carchult sxcenton Sanllped satateeSe->getHessageOD:thisesanmon t sthne. "Tinacaraosostiinaes corasodlo=custom.logtravelosA console (STAGING)7827051—780709733712713714717-729734736TX: AUTONAND a.created_at › DATE_:GROUP BY u.id, u.email,ORDER BY sns_count DESCselect + Eron teans, wherlselect + Eron coles,SELECTCONCATU.Id, CASE WHsa.*t.ouner id FROM sociJOIN users u on u.id = sJOIN teans tWHERE u.tean_id = 1117 aSELECT * FROM activitioSELECT * FROM actávitiosSELECT * FROM crn configSELECT # FROM teans WHERselleetron usens whenselect * fron playbooksseleetron miawhonkeselect * fron playbook_ctseleet ron eoeselect * fron crn_field.vSELECT * FROM crn_field_AOTM con Brelds 4 0M 4• JOIN activities a ON $NHERF Actvity "d = 7002AND € con onovider saSELECT * FROM activity.mselect * fron text relayMARAtA GAAR MANGUSAENOselect * fron users whenPARAtT GAAR MANGUREANOPAlRetGhAn dAAAIRdAMselect * fron teans wherPhat"toon actytysselect * fron activitysHoeDM:ActivityMonJiminnY …# backend# buescontusion-dinig# donut_time#enpineerine# generall# happy birthdavA infocoe intemal all# infra-chansed# infrastructure de#bu-team-infoiliminnv-bost olatform-team# platform-ticketstoroduct launche# random# released# suppor# thank-youssie the neoole of fimill6? Direct message.Vasil VasilenF8. Stefka Stoyanova1a llivana NetsevaA Ctovan TomnulPetko KashinskiGola Nimitrous: Todor Stamator1 Stelivan Georsie.& MireNikolav Vanko&. James GrahamLukas Kovalky#:AppsG tia Gloudai Gooele Calel1O0% LXo Inu co woy to:lc@ Describe what you are looking for€. Vasil Vasileve Messageru Add canvasu riiesLukas Kovalik 2:45 PMНе знам това дали е релевантно1. Shared Elasticsearch Sockets: Because pontl_fork® clones file descriptors, both child processes will share the exact same HTTP/TCP socket for Elasticsearch. Sinceyou didnit reset the es client in resetAfterFork (like you did for Redis and DB), the parallel workers will collide at the network layer, causing corrupted data and cURLexcenhons2. Supervisor Shutdown Deadlock: In reapthildren@.calling pcntl_waitpid@ without the WNOHANG flag makes it a blocking call. It one child process gets stuck (e.g..waiting on a network timeout), the parent supervisor will treeze indehnitely and completely ignore your 10-second gracetul shutdown logic. (cdited)3. Non-Atomic Redis Locke. The new Reindexmetrocicorer ceneoe winl oet sinnencelestwinl ser.Weeuse the workeste ruvorellelnev cboth read null at the exact same microsecond and both puch duplicate metrics to Datados. This needs to be a sinele, atomic command (e.c. set with Nxà).Vasil Vasiley 2-56 PM•с ло0ьо комcнтар2 e intentionalЗ сьщо e intentional поради факта, че phpredis driver-а не поддьржа set ex +nх параметри, и трябва да се пуснат две операции set + expirmuse Kousll4 4.07.01/ок, аз го пуснах пак през geminза третата точка обаче наистина ли няма подържкаARMhnCH CH BOORRAM HAK:OGили е друг driverVasil Vasiley 2-10pwHe Gaaa.oay co MannaRиn TаКа,М GO СUМTMLukas Kovalik 3.10 pNVasil Vasiley 3411pvoears nonuooka set Kevalve.nxe tueexeouc phpredis не ставаLukas Kovalik 3-11 PMтака май не работеше Sresult = Redis-set(StestKey, StestValue, 'EX", Sttl, "NX"): а така na Sresult = Redisuset(StestKey, StestValue, 'nx, 'ex' => Stti).Vacil Vaciley aunowвторото пробвах, и не станаTukse Kouslly 2.44.01nuahnatuaне помня вридVasil Vasilev 3.12 PMOATTHOTO MONA NAALukas Kovalik 3412.pNможе и ли боькамMessage Vasil Vasiley...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85207
|
2918
|
56
|
2026-05-28T12:12:27.454084+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970347454_m1.jpg...
|
PhpStorm
|
faVsco.js – JiminnyDebugCommand.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O &4‹$0(ah]БГ100% <78• Thu 28 May 15:12:27181ec2-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
|
7174956480566309325
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O &4‹$0(ah]БГ100% <78• Thu 28 May 15:12:27181ec2-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
|
NULL
|
NULL
|
NULL
|
|
85177
|
2919
|
45
|
2026-05-28T12:11:21.578360+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970281578_m2.jpg...
|
PhpStorm
|
faVsco.js – JiminnyDebugCommand.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocFV faVsco.s ~#12121 on JY-20963-fx- rapstomViewCoocFV faVsco.s ~#12121 on JY-20963-fx-lproidetcliwarduscomnhotenJiminhycocnee.corcom monoOiminhowchowooor© JiminnyTokeninfoCommandrOMaxeobexaweeocineC) ManageScimForTeam.choC) MarkBranchForEnvironmentPc Propadatecoaching Feed oacc) Purgeconterences. onoC Removeunused Pario oentsoC Resetsastesescch.ono.cRactorsCiamermotorancPineCeoeesolatacCaletitne noncCaneNidossynittoowt(© SyncActivity.pho(© Trackimported.php@ MhichWarkoeleWadinadolM SchedulineC) KernelpheMGontracte› @b Domain> CODTO> ê Emails.› Ca Enums› Ea Evente> E Exceptions> 0 FFMoeden FormatsHeloer.Tet Accessilokenprowidery controller•MAp• IMAle ScorndmAlteoonte338598858• Imsosline chteIOpportunn# PageIn CeMoAsthe> B SettingsWindowwnapers.oh©ActivityControlier.php© UsersHistory.phpOleomLorookeinuironekoackeac Jaainny console commandclass Jninnybebugcommand extends connanoprotected ssignature =1mnny:debug factzon?,"public function handle: voidi...onsware Funcrson testtern sseror woshnsossinfalt sinnor testand Rects Satmiith Phopeans suntayoStestKey = 'test:ratelinit:' . tine®:StestValue = (string) (tine() + 68):$ttl = 60;trySresult = Redis::set(StestKey. StestValue, 'EX'. Sttl, 'NX'):Sresult = Redis::set(StestKey, StestValue, I'nx'. 'ex' => $ttlip:if (Sresult)7 Redis SET with PhoRedis syntax succeeded).Cahtenintaste"Value: (StestValue)*):sthis-sintod"Tusirr secondsSretrtevedValue = Redis:"ger stestxev"Sthis->info( string: "Retrieved value: (SretcievedValue}"):Redis::del(Stestkey)Sthis-sinfodstVtest key deleted"else 1Sthis-serror( string: 'X Rediis SET returned false (key nay already exist)»):carchult sxcenton Sanllped satateeSe->getHessageOD:thisesenmon i sthoo: Tinacar 1aosostiinaes corasodloo Inu co woy 1o:1.2TextRelayServiceTest+0.A SF fiminny@localhost)No a-created at > DAT: SUBCNOWOL INTERVAL XA DAYSROUP BY u.id, u.email, u.name, u.softphone_numberconsole (PROD) x C Service.php# console fiaumDojminnyv045 A1 A41 У 66 ASELECTCONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN " (ouner)* ELSE "* END) AS user_id,activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a818Sc') = uuid; # 80186192 NgCacaado CodoxoKick off a new proinet. Make chanod.•I0TM actvs tes a 0M Edlactuty sdsaradMARAN & GAAR MAVE AOTONG WHAAA AMATAA AE T ENODALOG OENCARAN & GAAR CARGUSAENO WhOAA MIONA SA TM CRARG AOAGAN GAA AAAAIAAA4 1200/-06,091 Andhn ku SA AneACARAt& GAAs MenRd Whhnd WOe dACA CAA dA TM Aodds 4707/ 2420select * fron activities where user id = 7168 order by id desc Linit 10:select * fron activity searches where user_1d = 31954:activity search filters where activity_search_id TN (88882. 88902)eEnexthal Sanha yC Multi-Picklist Layout Displat emahienuaino PodeswiettR minutes aoo789710RRARRENwoeurleimeSnhiesensd....
|
NULL
|
507805259307536302
|
NULL
|
click
|
ocr
|
NULL
|
rapstomViewCoocFV faVsco.s ~#12121 on JY-20963-fx- rapstomViewCoocFV faVsco.s ~#12121 on JY-20963-fx-lproidetcliwarduscomnhotenJiminhycocnee.corcom monoOiminhowchowooor© JiminnyTokeninfoCommandrOMaxeobexaweeocineC) ManageScimForTeam.choC) MarkBranchForEnvironmentPc Propadatecoaching Feed oacc) Purgeconterences. onoC Removeunused Pario oentsoC Resetsastesescch.ono.cRactorsCiamermotorancPineCeoeesolatacCaletitne noncCaneNidossynittoowt(© SyncActivity.pho(© Trackimported.php@ MhichWarkoeleWadinadolM SchedulineC) KernelpheMGontracte› @b Domain> CODTO> ê Emails.› Ca Enums› Ea Evente> E Exceptions> 0 FFMoeden FormatsHeloer.Tet Accessilokenprowidery controller•MAp• IMAle ScorndmAlteoonte338598858• Imsosline chteIOpportunn# PageIn CeMoAsthe> B SettingsWindowwnapers.oh©ActivityControlier.php© UsersHistory.phpOleomLorookeinuironekoackeac Jaainny console commandclass Jninnybebugcommand extends connanoprotected ssignature =1mnny:debug factzon?,"public function handle: voidi...onsware Funcrson testtern sseror woshnsossinfalt sinnor testand Rects Satmiith Phopeans suntayoStestKey = 'test:ratelinit:' . tine®:StestValue = (string) (tine() + 68):$ttl = 60;trySresult = Redis::set(StestKey. StestValue, 'EX'. Sttl, 'NX'):Sresult = Redis::set(StestKey, StestValue, I'nx'. 'ex' => $ttlip:if (Sresult)7 Redis SET with PhoRedis syntax succeeded).Cahtenintaste"Value: (StestValue)*):sthis-sintod"Tusirr secondsSretrtevedValue = Redis:"ger stestxev"Sthis->info( string: "Retrieved value: (SretcievedValue}"):Redis::del(Stestkey)Sthis-sinfodstVtest key deleted"else 1Sthis-serror( string: 'X Rediis SET returned false (key nay already exist)»):carchult sxcenton Sanllped satateeSe->getHessageOD:thisesenmon i sthoo: Tinacar 1aosostiinaes corasodloo Inu co woy 1o:1.2TextRelayServiceTest+0.A SF fiminny@localhost)No a-created at > DAT: SUBCNOWOL INTERVAL XA DAYSROUP BY u.id, u.email, u.name, u.softphone_numberconsole (PROD) x C Service.php# console fiaumDojminnyv045 A1 A41 У 66 ASELECTCONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN " (ouner)* ELSE "* END) AS user_id,activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a818Sc') = uuid; # 80186192 NgCacaado CodoxoKick off a new proinet. Make chanod.•I0TM actvs tes a 0M Edlactuty sdsaradMARAN & GAAR MAVE AOTONG WHAAA AMATAA AE T ENODALOG OENCARAN & GAAR CARGUSAENO WhOAA MIONA SA TM CRARG AOAGAN GAA AAAAIAAA4 1200/-06,091 Andhn ku SA AneACARAt& GAAs MenRd Whhnd WOe dACA CAA dA TM Aodds 4707/ 2420select * fron activities where user id = 7168 order by id desc Linit 10:select * fron activity searches where user_1d = 31954:activity search filters where activity_search_id TN (88882. 88902)eEnexthal Sanha yC Multi-Picklist Layout Displat emahienuaino PodeswiettR minutes aoo789710RRARRENwoeurleimeSnhiesensd....
|
85175
|
NULL
|
NULL
|
NULL
|
|
85161
|
2919
|
37
|
2026-05-28T12:10:55.865368+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970255865_m2.jpg...
|
PhpStorm
|
faVsco.js – JiminnyDebugCommand.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
Sync Changes
Hide This Notification
Code changed:
Hide
2
11
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
/**
* Class JiminnyDebugCommand
*
* @package Jiminny\Console\Commands
*/
class JiminnyDebugCommand extends Command
{
protected $signature = 'jiminny:debug {action?}';
public function handle(): void
{
$action = $this->argument('action');
if ($action === 'redis-set') {
$this->testRedisSet();
return;
}
exit(1);
$team = Team::findOrFail(2);
$usersIds = $team->users()->pluck('users.id')->toArray();
var_dump($usersIds);
die;
exit(1);
}
private function testRedisSet(): void
{
$this->info('Testing Redis SET with PhpRedis syntax...');
$testKey = 'test:ratelimit:' . time();
$testValue = (string) (time() + 60);
$ttl = 60;
try {
// $result = Redis::set($testKey, $testValue, 'EX', $ttl, 'NX');
$result = Redis::set($testKey, $testValue, ['nx', 'ex' => $ttl]);
if ($result) {
$this->info('✅ Redis SET with PhpRedis syntax succeeded');
$this->info("Key: {$testKey}");
$this->info("Value: {$testValue}");
$this->info("TTL: {$ttl} seconds");
$retrievedValue = Redis::get($testKey);
$this->info("Retrieved value: {$retrievedValue}");
Redis::del($testKey);
$this->info('✅ Test key deleted');
} else {
$this->error('❌ Redis SET returned false (key may already exist)');
}
} catch (\Exception $e) {
$this->error('❌ Redis SET failed: ' . $e->getMessage());
$this->error('Trace: ' . $e->getTraceAsString());
}
}
}
Execute...
|
[{"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":"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.3693484,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","depth":4,"bounds":{"left":0.3793218,"top":0.15003991,"width":0.008976064,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","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\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\n\n/**\n * Class JiminnyDebugCommand\n *\n * @package Jiminny\\Console\\Commands\n */\nclass JiminnyDebugCommand extends Command\n{\n protected $signature = 'jiminny:debug {action?}';\n\n public function handle(): void\n {\n $action = $this->argument('action');\n\n if ($action === 'redis-set') {\n $this->testRedisSet();\n\n return;\n }\n\n exit(1);\n\n $team = Team::findOrFail(2);\n $usersIds = $team->users()->pluck('users.id')->toArray();\n\n var_dump($usersIds);\n die;\n exit(1);\n }\n\n private function testRedisSet(): void\n {\n $this->info('Testing Redis SET with PhpRedis syntax...');\n\n $testKey = 'test:ratelimit:' . time();\n $testValue = (string) (time() + 60);\n $ttl = 60;\n\n try {\n // $result = Redis::set($testKey, $testValue, 'EX', $ttl, 'NX');\n $result = Redis::set($testKey, $testValue, ['nx', 'ex' => $ttl]);\n\n if ($result) {\n $this->info('✅ Redis SET with PhpRedis syntax succeeded');\n $this->info(\"Key: {$testKey}\");\n $this->info(\"Value: {$testValue}\");\n $this->info(\"TTL: {$ttl} seconds\");\n\n $retrievedValue = Redis::get($testKey);\n $this->info(\"Retrieved value: {$retrievedValue}\");\n\n Redis::del($testKey);\n $this->info('✅ Test key deleted');\n } else {\n $this->error('❌ Redis SET returned false (key may already exist)');\n }\n } catch (\\Exception $e) {\n $this->error('❌ Redis SET failed: ' . $e->getMessage());\n $this->error('Trace: ' . $e->getTraceAsString());\n }\n }\n}","depth":4,"bounds":{"left":0.13364361,"top":0.0,"width":0.2805851,"height":1.0},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Jiminny\\Models\\Team;\n\n/**\n * Class JiminnyDebugCommand\n *\n * @package Jiminny\\Console\\Commands\n */\nclass JiminnyDebugCommand extends Command\n{\n protected $signature = 'jiminny:debug {action?}';\n\n public function handle(): void\n {\n $action = $this->argument('action');\n\n if ($action === 'redis-set') {\n $this->testRedisSet();\n\n return;\n }\n\n exit(1);\n\n $team = Team::findOrFail(2);\n $usersIds = $team->users()->pluck('users.id')->toArray();\n\n var_dump($usersIds);\n die;\n exit(1);\n }\n\n private function testRedisSet(): void\n {\n $this->info('Testing Redis SET with PhpRedis syntax...');\n\n $testKey = 'test:ratelimit:' . time();\n $testValue = (string) (time() + 60);\n $ttl = 60;\n\n try {\n // $result = Redis::set($testKey, $testValue, 'EX', $ttl, 'NX');\n $result = Redis::set($testKey, $testValue, ['nx', 'ex' => $ttl]);\n\n if ($result) {\n $this->info('✅ Redis SET with PhpRedis syntax succeeded');\n $this->info(\"Key: {$testKey}\");\n $this->info(\"Value: {$testValue}\");\n $this->info(\"TTL: {$ttl} seconds\");\n\n $retrievedValue = Redis::get($testKey);\n $this->info(\"Retrieved value: {$retrievedValue}\");\n\n Redis::del($testKey);\n $this->info('✅ Test key deleted');\n } else {\n $this->error('❌ Redis SET returned false (key may already exist)');\n }\n } catch (\\Exception $e) {\n $this->error('❌ Redis SET failed: ' . $e->getMessage());\n $this->error('Trace: ' . $e->getTraceAsString());\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.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}]...
|
-4769198993919478972
|
-1289902856579339838
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
2
11
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
use Jiminny\Models\Team;
/**
* Class JiminnyDebugCommand
*
* @package Jiminny\Console\Commands
*/
class JiminnyDebugCommand extends Command
{
protected $signature = 'jiminny:debug {action?}';
public function handle(): void
{
$action = $this->argument('action');
if ($action === 'redis-set') {
$this->testRedisSet();
return;
}
exit(1);
$team = Team::findOrFail(2);
$usersIds = $team->users()->pluck('users.id')->toArray();
var_dump($usersIds);
die;
exit(1);
}
private function testRedisSet(): void
{
$this->info('Testing Redis SET with PhpRedis syntax...');
$testKey = 'test:ratelimit:' . time();
$testValue = (string) (time() + 60);
$ttl = 60;
try {
// $result = Redis::set($testKey, $testValue, 'EX', $ttl, 'NX');
$result = Redis::set($testKey, $testValue, ['nx', 'ex' => $ttl]);
if ($result) {
$this->info('✅ Redis SET with PhpRedis syntax succeeded');
$this->info("Key: {$testKey}");
$this->info("Value: {$testValue}");
$this->info("TTL: {$ttl} seconds");
$retrievedValue = Redis::get($testKey);
$this->info("Retrieved value: {$retrievedValue}");
Redis::del($testKey);
$this->info('✅ Test key deleted');
} else {
$this->error('❌ Redis SET returned false (key may already exist)');
}
} catch (\Exception $e) {
$this->error('❌ Redis SET failed: ' . $e->getMessage());
$this->error('Trace: ' . $e->getTraceAsString());
}
}
}
Execute...
|
85159
|
NULL
|
NULL
|
NULL
|
|
85160
|
2918
|
32
|
2026-05-28T12:10:55.458045+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970255458_m1.jpg...
|
PhpStorm
|
faVsco.js – JiminnyDebugCommand.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...
|
[{"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}]...
|
-5714658179048184897
|
-8634297297851446848
|
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
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"O &4‹$0(ah]A100% <78• Thu 28 May 15:10:54181ec2-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
|
NULL
|
NULL
|
NULL
|
|
85159
|
2919
|
36
|
2026-05-28T12:10:53.926466+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970253926_m2.jpg...
|
PhpStorm
|
faVsco.js – JiminnyDebugCommand.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-209 rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-lproidetwnapers.oh© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.pho© ProfileObserver.phgC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc> (i PoliciesuProvider• Queuea Reoositonea RulesaServicesDa ActivityAiReodnleallendarIiconnerimermobiacteDecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyMAetioneD ContactSyncStrateay› DDTO> FieldshlaurndOLekоkyseiwico.on© Kernel.phgMWiaKorCass wochexeOpportunivsvncstratcov> ea PacinationaProsoec SearchStrateo→- RedisService traits• Utils- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCleantone@ GiosedDealStanesSeniceclnaalise rccarica ono* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)ScacheKey = Sthis->getRateLimitCacheKeyO=scachedcxpzresar = keozs..cetscachekey)if as string(ScachedExpiresAt) 8& is nuneric(ScachedExpiresat)Srenainznd = aax vau(int) ScachedExpiresAt - tineOn:throw new RateLinitExceptionnessase: Huospot rate uinzx cached circuz-breaker")Srenazningretuen Santmaiero"carch aihcomable Sei6 (SthisosisHubsnotPateLinit(se)) €Sretry.terihisornarse?erny.ter e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 429s in the sane burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redhis..setScachekey strinall trabl4 Soetaulster• ExtractStnIs->L0g->warnIngCTHUOSpOT) Received 429 from API". (= Sthssosconttaosteae5al=> Sthis->confio-sgetido'retry after' => SretryAfter.-CWAeMANAAAYthrow new RateLimitException( message: "Hubspot returned 429'. SretcyAfter. Se):throw serpnivate functiion getRateLimitCachekev@: stoinof.coother anousoae i minutas soo•• Thu 28 May 15:10:53TextRelayServiceTest+0.A SF fiminny@localhost)A console [STAGING)D6.00€ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYITGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER RY sns count795704BEBIEE714-215select * fron teans where id = 1CEIEOTCONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN " ouner' ELSE ** END) AS user idt.ouner id FROM social accounts saJOIN users u on u.id = sa.sociable icJOIN teans t1.n<->1: on t.id = u.team_idNHERE u.tean_id = 1117 and sa.provider = *hubspot"d console (PROD) x C Service.php# console fiaumDojminnyv045 A1 A41 У 66 ACachado Codo XoKick off a new proinet. Make chanod.726=732- 735SELECT * FROM Crn.field_data fd* JOIN crn_fields f ON fd.crm_field_id = f.1o* JOIN activities a ON fd.activity_id = a.1dWHERE activity_id = 79933459• AND f.crn_provider_id = 'hs_activity_type';select * fron text_ relays where created at > *2826-85-81*:MARAt" GAAR NANRURASNe WhNAA MONA SA TM CRAROselect * fron users where team id = 1 and id IN ((18688. 13934. 7160):select * fron activities where user id = 7168 order by id desc Linit 10.ohettene neaed whond coedpue locnhootl. m24004449select * fron teans where id = 1117select * fron activity searches where user_1d = 31954:Pohettendactivity search_filters where activity_search._id TN (88882. 88992)eEnexthal Sanha yC Multi-Picklist Layout Displantamahieuato Podeswiettwwoceueta100-07 132R chars.3 Foe braskel UTE-яensd....
|
NULL
|
720081977507203342
|
NULL
|
click
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-209 rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-lproidetwnapers.oh© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.pho© ProfileObserver.phgC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc> (i PoliciesuProvider• Queuea Reoositonea RulesaServicesDa ActivityAiReodnleallendarIiconnerimermobiacteDecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyMAetioneD ContactSyncStrateay› DDTO> FieldshlaurndOLekоkyseiwico.on© Kernel.phgMWiaKorCass wochexeOpportunivsvncstratcov> ea PacinationaProsoec SearchStrateo→- RedisService traits• Utils- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCleantone@ GiosedDealStanesSeniceclnaalise rccarica ono* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)ScacheKey = Sthis->getRateLimitCacheKeyO=scachedcxpzresar = keozs..cetscachekey)if as string(ScachedExpiresAt) 8& is nuneric(ScachedExpiresat)Srenainznd = aax vau(int) ScachedExpiresAt - tineOn:throw new RateLinitExceptionnessase: Huospot rate uinzx cached circuz-breaker")Srenazningretuen Santmaiero"carch aihcomable Sei6 (SthisosisHubsnotPateLinit(se)) €Sretry.terihisornarse?erny.ter e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 429s in the sane burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redhis..setScachekey strinall trabl4 Soetaulster• ExtractStnIs->L0g->warnIngCTHUOSpOT) Received 429 from API". (= Sthssosconttaosteae5al=> Sthis->confio-sgetido'retry after' => SretryAfter.-CWAeMANAAAYthrow new RateLimitException( message: "Hubspot returned 429'. SretcyAfter. Se):throw serpnivate functiion getRateLimitCachekev@: stoinof.coother anousoae i minutas soo•• Thu 28 May 15:10:53TextRelayServiceTest+0.A SF fiminny@localhost)A console [STAGING)D6.00€ANo a-created at> DAT: SUBCMONOL "INTERVAL XA DAYITGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER RY sns count795704BEBIEE714-215select * fron teans where id = 1CEIEOTCONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN " ouner' ELSE ** END) AS user idt.ouner id FROM social accounts saJOIN users u on u.id = sa.sociable icJOIN teans t1.n<->1: on t.id = u.team_idNHERE u.tean_id = 1117 and sa.provider = *hubspot"d console (PROD) x C Service.php# console fiaumDojminnyv045 A1 A41 У 66 ACachado Codo XoKick off a new proinet. Make chanod.726=732- 735SELECT * FROM Crn.field_data fd* JOIN crn_fields f ON fd.crm_field_id = f.1o* JOIN activities a ON fd.activity_id = a.1dWHERE activity_id = 79933459• AND f.crn_provider_id = 'hs_activity_type';select * fron text_ relays where created at > *2826-85-81*:MARAt" GAAR NANRURASNe WhNAA MONA SA TM CRAROselect * fron users where team id = 1 and id IN ((18688. 13934. 7160):select * fron activities where user id = 7168 order by id desc Linit 10.ohettene neaed whond coedpue locnhootl. m24004449select * fron teans where id = 1117select * fron activity searches where user_1d = 31954:Pohettendactivity search_filters where activity_search._id TN (88882. 88992)eEnexthal Sanha yC Multi-Picklist Layout Displantamahieuato Podeswiettwwoceueta100-07 132R chars.3 Foe braskel UTE-яensd....
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85158
|
2918
|
31
|
2026-05-28T12:10:53.823224+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970253823_m1.jpg...
|
PhpStorm
|
faVsco.js – JiminnyDebugCommand.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"84‹$0(ah]A100% <78• Thu 28 May 15:10:53181ec2-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
|
-993742772756769989
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpec2-user@ip-10-30-129-190:~screenpipe"84‹$0(ah]A100% <78• Thu 28 May 15:10:53181ec2-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...
|
85157
|
NULL
|
NULL
|
NULL
|
|
85157
|
2918
|
30
|
2026-05-28T12:10:51.874715+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970251874_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpDOCKER#_####_#####\###1\#/V~'881DEV (-zsh)O 82-zshN3ec2-user@ip-10-30-129-190:~screenpipe"-zsh‹ >0 lol85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 15:10:51181ec2-user@ip-10-30-140-…..₴7Amazon Linux 2023 (ECS Optimized)For documentation, visit [URL_WITH_CREDENTIALS] ~]$exitlogoutConnection to jiminny-prod-ecs1closed.lukccdlulenWAPS$131FirefoxVersion 2023.11.20260505:Version2023.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)V~/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) $D...
|
NULL
|
-1505875326795226986
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpDOCKER#_####_#####\###1\#/V~'881DEV (-zsh)O 82-zshN3ec2-user@ip-10-30-129-190:~screenpipe"-zsh‹ >0 lol85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 15:10:51181ec2-user@ip-10-30-140-…..₴7Amazon Linux 2023 (ECS Optimized)For documentation, visit [URL_WITH_CREDENTIALS] ~]$exitlogoutConnection to jiminny-prod-ecs1closed.lukccdlulenWAPS$131FirefoxVersion 2023.11.20260505:Version2023.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)V~/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) $D...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85156
|
2919
|
35
|
2026-05-28T12:10:52.719236+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970252719_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
Execute
Explain Plan
Browse Query 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","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":"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.36801863,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"bounds":{"left":0.37799203,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","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;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
-4036707360888411972
|
5225835645296838756
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
Execute
Explain Plan
Browse Query History...
|
85154
|
NULL
|
NULL
|
NULL
|
|
85146
|
2919
|
29
|
2026-05-28T12:10:15.692510+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970215692_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
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 ...
|
[{"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":"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.36801863,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"bounds":{"left":0.37799203,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","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;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\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 *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\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->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
-3264450658369633090
|
1136716483744153188
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
2
69
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), 'EX', $retryAfter, 'NX');
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
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.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
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->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\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);
}
}
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 ...
|
85145
|
NULL
|
NULL
|
NULL
|
|
85145
|
2919
|
28
|
2026-05-28T12:10:13.982925+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779970213982_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-209 rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-proidetwnapers.oh© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc> (i PoliciesuProvider• Queuea Reoositonea RulesaServicesDa ActivityAiReodrlealendarIiconnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyIhctiontD ContactSyncStrateay• D DTO› FieldsThtauendOLekоkyseiwico.on© Kernel.phgC Client.php >MWiaKorCass wochexeR82* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)Opportunivsvncstratcov- Pac nation- Prosoec-SearchStrateoScacheKey = Sthis->getRateLimitCacheKeyOScacheotxozresac = keozst.oet scachekey)*if (is_string(ScachedExpiresAt) && is_numeric(ScachedExpiresAt)) 1Sremaining = naxl vax(cint)) ScachedExpiresAt - tineon:hrow new kacetinzcexceoezonsrenaining,try EneruonSantwaittos}catch (Throwable Se) (i4 (Sthiso>isHubsootRateLinit(Se))merryAttenthisorrarse?erny.ter e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 42%s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redis::set(ScacheKey, (string) (tine() + SretryAfter).Sthis->log->warning( Hubspot) Received 429 from API'.iteanSdl'configid'= Sthis->config->getido.ns Scetryatter-ENAeMAONNAT→- Redisthrow new RateLinitException message: "Hubspot returned 429'. SretryAfter. Se):Service traits• UtilsThrow Sen- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCleantone@ GiosedDealStaapsSeniceclnaalise rccarica onoprivate function getRateLinitCachekevO: strinol...;oo Inu co moy to.lo:TextRelayServiceTest+0.Setm onwatoes noesld console (PROD) x C Service.php# console fiaumA console [STAGING)D6.00€ANo a-created at > DAT: SUBCNOWOL IINTERVAL 3E DAYIIGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER BY sne count DESCdDojminnyv793704E714-215-717TARNRCBER728E732—730- 735045 A1 A41 У 66 Aselect + Eron teans whenp dm15C3IEACONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN " ouner' ELSE ** END) AS usen idt.ouner id FROM social accounts saJOIN users u on u.id = sa.sociable icJOIN teans t1.n<->1: on t.id = u.team_idNHERE u.tean_id = 1117 and sa.provider = *hubspot"SaLaeT * FRON accvaes whEkE uuzdcO b10 [CREDIT_CARD]-[CREDIT_CARD] = UU1d: # 79953459 Y28SEIFM + EROM eo Geld data&•JOTM con Belds 4fd.crm_field_id = f.10• JOIN activities a ON fd.activity_id = a.1dWHERE activity_id = 79933459MARAt"GAAR NANGURASNS WhNA NONn SA TM CRARO18688) and created at > '2026-05-22' order by id descid IN ((18688. 13934. 7160):PARAtT GAAR MANGURAANOSid = 7168 order by id desc Linit 10:select * fron teans where id = 1117select * fron activity searches where user_1d = 31954:Pohettendactivity search_filters where actiivity_search_id TN (88882. 88992)Cacaado CodoxoKick off a new proinet. Make chanod.c. Fix Texikelay service TesteC Multi-Picklist Layout Displantamahieuato Podeswiettohr ianousone minutas sodutwoderTesme axelhi.%ensd....
|
NULL
|
7498707782984038678
|
NULL
|
click
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-209 rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-proidetwnapers.oh© VoiceConsentPrefix.phpNotificationsv Observers© AccountObserver.phpCyActwwooserver.pnp© ContactObserver.phgGroupObserver.pho© LeadObserver.phoC ProfileObserver.phpC SocialAccountObserver.phg© UserObserver.pho© UserRoleObserver.phc> (i PoliciesuProvider• Queuea Reoositonea RulesaServicesDa ActivityAiReodrlealendarIiconnerimermobiacte@ DecorateActivityDummym Helnarsv D Hubspor(i) AccountSyncStrategyIhctiontD ContactSyncStrateay• D DTO› FieldsThtauendOLekоkyseiwico.on© Kernel.phgC Client.php >MWiaKorCass wochexeR82* Breturn T The result of the APi callprivate function executeRequest(callable SapiCall)Opportunivsvncstratcov- Pac nation- Prosoec-SearchStrateoScacheKey = Sthis->getRateLimitCacheKeyOScacheotxozresac = keozst.oet scachekey)*if (is_string(ScachedExpiresAt) && is_numeric(ScachedExpiresAt)) 1Sremaining = naxl vax(cint)) ScachedExpiresAt - tineon:hrow new kacetinzcexceoezonsrenaining,try EneruonSantwaittos}catch (Throwable Se) (i4 (Sthiso>isHubsootRateLinit(Se))merryAttenthisorrarse?erny.ter e// NX: only the first job to receive a 429 in this burst sets the key.// Subsequent 42%s in the same burst leave the TTL untouched so theIl window is not reset by every concurrent job hitting the linit.Redis::set(ScacheKey, (string) (tine() + SretryAfter).Sthis->log->warning( Hubspot) Received 429 from API'.iteanSdl'configid'= Sthis->config->getido.ns Scetryatter-ENAeMAONNAT→- Redisthrow new RateLinitException message: "Hubspot returned 429'. SretryAfter. Se):Service traits• UtilsThrow Sen- Wechookc) Batchsunceo ector cho© BatchSvncRedisService.oCleantone@ GiosedDealStaapsSeniceclnaalise rccarica onoprivate function getRateLinitCachekevO: strinol...;oo Inu co moy to.lo:TextRelayServiceTest+0.Setm onwatoes noesld console (PROD) x C Service.php# console fiaumA console [STAGING)D6.00€ANo a-created at > DAT: SUBCNOWOL IINTERVAL 3E DAYIIGROUP BY v.id, v.email, u.nane, u.softphone_numberORDER BY sne count DESCdDojminnyv793704E714-215-717TARNRCBER728E732—730- 735045 A1 A41 У 66 Aselect + Eron teans whenp dm15C3IEACONCAT(u.1d, CASE WHEN u.id = t.ouner id THEN " ouner' ELSE ** END) AS usen idt.ouner id FROM social accounts saJOIN users u on u.id = sa.sociable icJOIN teans t1.n<->1: on t.id = u.team_idNHERE u.tean_id = 1117 and sa.provider = *hubspot"SaLaeT * FRON accvaes whEkE uuzdcO b10 [CREDIT_CARD]-[CREDIT_CARD] = UU1d: # 79953459 Y28SEIFM + EROM eo Geld data&•JOTM con Belds 4fd.crm_field_id = f.10• JOIN activities a ON fd.activity_id = a.1dWHERE activity_id = 79933459MARAt"GAAR NANGURASNS WhNA NONn SA TM CRARO18688) and created at > '2026-05-22' order by id descid IN ((18688. 13934. 7160):PARAtT GAAR MANGURAANOSid = 7168 order by id desc Linit 10:select * fron teans where id = 1117select * fron activity searches where user_1d = 31954:Pohettendactivity search_filters where actiivity_search_id TN (88882. 88992)Cacaado CodoxoKick off a new proinet. Make chanod.c. Fix Texikelay service TesteC Multi-Picklist Layout Displantamahieuato Podeswiettohr ianousone minutas sodutwoderTesme axelhi.%ensd....
|
NULL
|
NULL
|
NULL
|
NULL
|