|
43794
|
1596
|
15
|
2026-05-14T13:16:04.003544+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764564003_m2.jpg...
|
PhpStorm
|
faVsco.js – console_7 [QAI PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
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
Editor for console_7.sql
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
DealInsights, folder
Security, folder
TeamInsights, folder
ActivityActualDate.php
ActivityChannel.php...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\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.40957448,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.41821808,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.42918882,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.43783244,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.44647607,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.4574468,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.46841756,"top":0.123703115,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4950133,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.50598407,"top":0.123703115,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.7084442,"top":0.123703115,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for console_7.sql","depth":4,"bounds":{"left":0.42719415,"top":0.1452514,"width":0.31083778,"height":0.8347965},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"InviteUserToTeamAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkUserAsOnboardableAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncRecordingFlagsAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateTeamMemberAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateUserRolesAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EventSubscriber, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinition, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Security, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityActualDate.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityChannel.php","depth":11,"on_screen":false,"role_description":"text"}]...
|
-2982445106945629625
|
-6724478313937337938
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
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
Editor for console_7.sql
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
DealInsights, folder
Security, folder
TeamInsights, folder
ActivityActualDate.php
ActivityChannel.php...
|
43792
|
NULL
|
NULL
|
NULL
|
|
43793
|
1595
|
15
|
2026-05-14T13:16:04.003523+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764564003_m1.jpg...
|
PhpStorm
|
faVsco.js – console_7 [QAI PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
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
Editor for console_7.sql
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\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":false,"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":"AXTextArea","text":"Editor for console_7.sql","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"InviteUserToTeamAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkUserAsOnboardableAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncRecordingFlagsAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateTeamMemberAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateUserRolesAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EventSubscriber, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinition, folder","depth":10,"on_screen":false,"role_description":"text"}]...
|
-891976454116103706
|
-6706472713668353553
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
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
Editor for console_7.sql
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder...
|
43790
|
NULL
|
NULL
|
NULL
|
|
43792
|
1596
|
14
|
2026-05-14T13:16:00.708903+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764560708_m2.jpg...
|
PhpStorm
|
faVsco.js – console_7 [QAI PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2866196027416673746
|
-8132372713639400506
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
PhostormcodeFV faVsco.js#12077 on JY-20903-update_activity-stage-on...hProleteyCheckAndRetryRemoteMatch.phpC) DeleteForCoachecComClient.phpDownloaamissing lrackc rixAcuivitiesopportunitC) HaraDeleteActivitiestol© Matchermobiect.ong© HardDeleteActivitiesTem A12 A Vc) hyoratecalwincrmba© HydrateDefaultActivitviC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(C) MonitorMeetingcounte© MonitorMeetingEndCorC) MonitorMeetinostartco(t) MonitorMeetinaTrait.ohC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentseC) RoaccianTranscrintCom© ReindexRecentActivitie© RetryProspectsummary 106© SetProviderCapabilitiesc) StatusCount.ono© SyncMissingCallDisposi 10%© UpdateActivityElasticS€ 104© UpdateElasticSearch.pl• S Analvucs_ calendars> C Hubspot• _ IntegrationApp> D Traitsc) AddLavoutEntities.ono• AutoloaDelavedComma© BackfillOpportunityUsel .o© BullhornCommandAbstt 100C) BullhornPinaCommand.© BullhornSearchCommar 190© BullhornSessionComma 205CheckActivityLoggable© CleanDuplicateFieldDat©FullSyncOpportunityColC) LoaActivitiesCommandl©ManageSyncStrategyCrC) MatchCrmOhiectsComr© MatchOpportunityActiv@) MiarateProvider.php© ProcessHubspotObject: 22g(e) DuracDelotodOnnortunclass HubspotPaqinationserv1cepublic function getPaginatedDataGenerator('total_records_fetched' => $state->totalRecords,'total_elapsed_seconds' => round(Sstate->getElapsedSecondsO,precision: 2),'average_seconds_per_request' => $state->requestCount > 0 ? round( num: $state->getElapsedSeconds©1):// Update reference parametersScotal = sscace->cotalsLasckecorala = sscate-›lasckecoraloprivate function shouldStopPagination(PaginationState $state, int SteamId): boolf...;orivate function handlePaginationStrateavoarray Soavload.array SdefaultFilter,PaginationState $state,int SresultsPerPage,int SteamId): array 1...}private function shouldSwitchToKeysetPagination(PaginationState $state, int SresultsPerPage): boolf...}private function validateTokenIfNeeded(Client Sclient, PaginationState $state): void{...}private function executeSearchRequest(Client Sclient, string $endpoint, array $payload, PaginationState $sprivate function updateLastRecordId(array Spage. PaginationState Sstate): void{...}lprivate function getNext0ffset(array Spage): intf...}private function logPaginationProgress(PaginationState Sstate. int $teamid. string Sendnoint): voidf...?orivate function calculateDelavInMicroseconds@• intf...?40 hl100% L2. Thu 14 May 16:16:00AskJiminnyReportActivityServiceTest v= custom.logfO scratch &.ison=laravel.logA SF jiminny@localhost]& HS_local [jiminny@localhost]& console [PROD]A console (EU]iii accounts (EU]fii stages (EU]tid teams (EU]© ImportBotRecordingJob.php© Activity.php© FixActivitiesOpportunity.phpA console_7 [QAI PROD] x © Opportunity.phgA console [STAGING)D6 9 0 g Tx:Auto v | UPlaygroundSo jiminnyDatabaseTEO SLOEE DDL →› A jiminny@localhost 1 of 3A PROD 1 of 2› A EU 1of 2› A STAGING 6 of 11> A QA 10f2> A QAi 1of2V & CAI PROD 10f2> d8 jiminny, Server Obiects> M virtual views 1nity-change /l View pull request (16 minutes ago.WN Windeurf Toame 14 UITC.9Aenasod...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43791
|
1596
|
13
|
2026-05-14T13:15:58.980591+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764558980_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7247396273940288256
|
-6706462543186320913
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide...
|
43789
|
NULL
|
NULL
|
NULL
|
|
43790
|
1595
|
14
|
2026-05-14T13:15:58.956617+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764558956_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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}]...
|
-1323124728022276791
|
-6706471614291467793
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43789
|
1596
|
12
|
2026-05-14T13:15:48.571221+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764548571_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43788
|
1596
|
11
|
2026-05-14T13:15:45.584524+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764545584_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1164001697841025739
|
-7442901678070857280
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
0(5)PhostormcodeFV faVsco.js#12077 on JY-20903-update_activity-stage-on...hProleteyCheckAndRetryRemoteMatch.phpC) DeleteForCoachecComClientonpDownloaamissing lrackc rixAcuivitiesopportunitC) HaraDeleteActivitiestol© Matchermobiect.ong© HardDeleteActivitiesTeA12 ^Vc) hyoratecalwincrmba© HydrateDefaultActivityiC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(C) MonitorMeetingcounte© MonitorMeetingEndCorC) MonitorMeetinostartco(t) MonitorMeetinaTrait.ohC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentseC) RoaccianTranscrintCom© ReindexRecentActivitie© RetryProspectsummary 106© SetProviderCapabilitiesc) StatusCount.ono© SyncMissingCallDisposi 10%© UpdateActivityElasticS€ 104© UpdateElasticSearch.pl• _ Analvucsw Calendars> C Hubspot• _ IntegrationApp> D Traitsc) AddLavoutEntities.ono• AutoloaDelavedComma© BackfillOpportunityUsel .o© BullhornCommandAbstt 100C) BullhornPinaCommand.© BullhornSearchCommar 199© BullhornSessionComma 205CheckActivityLoggable© CleanDuplicateFieldDat©FullSyncOpportunityColC) LoaActivitiesCommandl©ManageSyncStrategyCrC) MatchCrm@hiectsComr@ MatchOnnortunituAativ@) MiarateProvider.php© ProcessHubspotObject: 22g(e) DuracDelotodOnnortunclass HubspotPaqnationservicepublic function getPaginatedDataGenerator('total_records_fetched' => $state->totalRecords,'total_elapsed_seconds' => round(Sstate->getElapsedSeconds.precision: 2),'average_seconds_per_request' => $state->requestCount > 0 ? round( num: $state->getElapsedSeconds©D):// Update reference parametersScotal = sscace->cotalsLasckecorala = sscate-›lasckecoralaprivate function shouldStopPagination(PaginationState Sstate, int SteamId): boolf...;orivate funct.ion handlePaqinationStrateavarray Soavload.array $defaultFilter,PaginationState $state,int SresultsPerPage,int SteamId): array 1...}private function shouldSwitchToKeysetPagination(PaginationState $state, int SresultsPerPage): boolf...}private function validateTokenIfNeeded(Client Sclient, PaginationState $state): void{...}private function executeSearchRequest(Client Sclient, string $endpoint, array $payload, PaginationState $sprivate function updateLastRecordId(array Spage. PaginationState Sstate): void{...}private function getNext0ffset(array Spage): intf...}private function logPaginationProgress(PaginationState Sstate. int $teamid. string Sendnoint): voidf...?orivate function calculateDelavInMicroseconds@: intf..}unitv-chande /| View null requect (16 minutes ado)= custom.log= laravel.logA SF jiminny@localhost]& HS_local [jiminny@localhost]& console [PROD]A console (EU]in accounts cu)fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.onp© Activity.php© FixActivitiesOpportunity.php& console SlAGiNGclass rixacciviclesupoorcunlcy excenas commanoorivace tunccion processrarclcloancsCtIVItV actIvity)• void {10410%10810S1101111115117Steam = Sactivity->qetTeamosSuser = Sactivitv->aetUserOrSparticioants = Sactivitv->aetParticinants0:foreach (Sparticipants as Sparticipant) ‹if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {continue:SemailAddress = $participant->getEmailAddressO:if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) {alantalllotry {Sopportunity = $this->find0pportunityInCrm($crmService, SemailAddress, Suser->qetIdO):} catch (Throwable $e) {$this->error( string: 'Could not find opportunity: ' . $e->getMessage());sopportunity = null:if (Sopportunitv === null) {Sthis->resetActivitvipportunitvSactivitv):continue:Sactivitv->undatedr'onnontunitv id' => Sonnortunitv->aettdo1):Sthis->info( string: 'Opportunity updated for activity: ' . $activity->getIdO):m 82^40 hl100% L2. Thu 14 May 16:15:45AskJiminnyReportActivityServiceTest vDatabaseО..:TEGlOEÜDL →O› A jiminny@localhost 1 of 3A PROD 1 of 2› A EU 1of 24 STAGING 6 of 11• & OA 1of2A QAi 10f2V & CAI PROD 10f2> d8 jiminny; E Soruar Oouct—- 12911148Tusagennivato function findûnnontunitvinfnm/SonvicoIntonfaco CenmSenvico etnina ComailAddnece int SucenTd)• 2Annontuns.private function resetActivityOpportunity(Activity Sactivity): void{...}W Windsurf Teams 99:50 UTF-8 Po 4 spaces...
|
43786
|
NULL
|
NULL
|
NULL
|
|
43787
|
1595
|
13
|
2026-05-14T13:15:45.255067+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764545255_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
iTerm2ShellEditViewSessio Project: faVsco.js, menu
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(all14100% <78• Thu 14 May 16:15:45ec2-user@ip-10-30-145-148:~DOCKERDEV (-zsh)82APP (-zsh)83-zsh84screenpipe"• [EMAIL]/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414cec2-user@ip-10-20-…."docker-php-entrypoi..."ec2-user@ip-10-30-.*86 hoursagoUp6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp6 hours(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd889378006ad3430275ebamazon/amazon-ecs-agent: latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec2-user@ip-10-30-145-148 ~]$ docker exec -tidockerexec-ti f3c21482c23f /bin/bash -c "cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"docker exec -tidockereXeс-ti f3c21482c23f /bin/bash -c"cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"Error response from daemon: No suchcontainer: docker[ec2-user@ip-10-30-145-148 ~]$ docker psCONTAINER IDIMAGECOMMANDCREATEDSTATUSPORTSNAMESf3c21482c23ftes (healthy)055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi.."10 minutesagoUp 10 minu9000/tcpecs-worker-conferences-422-worker-conferences-e8cceec2ddffcbc31b00bc68a3f31570055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."tes (healthy)9000/tcpecs-worker-softphone-422-worker-softphone-eedfb1faf8d3ebe1aa0116 minutesagoUp 16 minuee859e8ab6b8055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."58 minutesagoUp 58 minutes (healthy)9000/tcpecs-worker-calendar-421-worker-calendar-b2b9ade8c08bd5ee520027cf2b644193055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."2 hours agoUp 2 hours(healthy)9000/tcpecs-worker-emails-421-worker-emails-f6bac583b7dbc1a01f00106706b6d1ac055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)a8fe3893c15b9000/tcpecs-worker-processing-3-421-worker-processing-3-fcdadbbbe0d788ea8201055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd88937800"docker-php-entrypoi..."6 hours agoUp 6 hours6ad3430275ebamazon/amazon-ecs-agent:latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec?-user@ip-10-30-145-148 ~]$ dockerexec -it $(docker ps --format "{{.ID}}" --filter "name=ecs-worker" | head -1) /bin/bash -c "cd /home/jiminny && bash"root@f3c21482c23f:/home/jiminny# php artisan crm:sync-opportunity--teamId=1052 --from='2022-01-01 00:00:00'--strategy lastModified[2026-05-14 13:15:17] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command" : "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryPeakBeforeCommandInMb":118.0} {"correlation_id":"75555791-00b0-4230-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}Syncing opportunity for Hubspot QAi[2026-05-14 13:15:18] qai.WARNING: [HubSpot] Account not connected for user {"userId": "682978a5-ee72-48b4-86ee-b8efdfc98cd7", "account" : {"Jiminny| \Models||SocialAccount" : {"id" :166,"sociable_id":23552,"provider_user_id":"45562061"., "expires":1778474185, "refresh_token_expires":null,"provider": "hubspot",,"state": "full-refresh", "auth_scope":null, "retry_after" : nul1, "created_at": "2026-03-20 07:38:26", "updated_at": "2026-05-11 04:33:30", "sociable_type": "user"3}} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id" :"5bb63d01-98a5-4а31-8833-e7891c8e4570"}Your HubSpotaccount has become disconnected. Please login to Jiminny to reconnect. skipping..[2026-05-14 13:15:18] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command": "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryAfterCommandInMB" :122.0, "memoryPeakBeforeCommandInMb" : 118.0, "memoryPeakAfterCommandInMB":122.0} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}root@f3c21482c23f:/home/jiminny#l...
|
43785
|
NULL
|
NULL
|
NULL
|
|
43786
|
1596
|
10
|
2026-05-14T13:15:42.103246+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764542103_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormVIewINavicarecodeLaravelKeractorWindowFV f PhpStormVIewINavicarecodeLaravelKeractorWindowFV faVsco.js#12077 on JY-20903-update_activity-stage-on...haProiect v) CheckAndRetryRemoteMatch.phpC) DeleteForCoachecComActvitesmatchermcommano.pnp© service.ong© Client.php© HubspotPaginationService.php x© MatchActivityCrmData.phpUpoatecrmbata.ongDownloaamissing lrackc rixAcuivitiesopportunit©) HaraDeleteActivitiesto© HardDeleteActivitiesTeclass nuospocracinatlonserviceB12^c) hyoratecalwincrmba© HydrateDefaultActivitviC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities( 78C) MonitorMeetingcounte© MonitorMeetingEndCorC) MonitorMeetinostartco(t) MonitorMeetinaTrait.orC) NotifvNotLoaded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.ptc) ProheMediaSeamentso@ Peascian TranccrintCon"C) ReindeyRecentActivitie: 100)© RetryProspectSummary 103(C) SetProviderCanabilitios 107(e) StatusCount.php© SyncMissingCallDisposi 104© UpdateActivityElasticSe 103© UpdateElasticSearch.pr 132• _ Analvucslaeellonaereyalermnl> C Hubspot> C IntegrationApp> D Traits© AddLavoutEntities.php© AutoloaDelavedComma 160(C) Backfill@pportunitvUsel 198C BullhornCommandAbstC) BullhornPinaCommand, 199© BullhornSearchCommar 209C) BullhornSessioncomma@ CheckActivitvl.oagablei 206C) CleanDunlicateSieldDat 210.©FullSyncOpportunityCoC) 1oaActivitiecCommand. 21g© ManageSyncStrategyCi 223C) MatchCrm@hiectsComr@ MatchOnnortunituAativ© MigrateProvider.php© ProcessHubspotObject: 229(e) DuracDelotodOnnortunOUDLLCoecracnaceavacabeneracor'total_requests' => $state->requestCount,itotal records fetched' => Sstate->totalRecords.'total_elapsed_seconds' => round(Sstate->getElapsedSeconds(, precision: 2),'average_seconds_per_request' => $state->requestCount > 0 ? round( num: Sstate->getElapsedSeconds / $state->requestCount, precision: 2) : 0,]):XlI to Cascade, xI to Command// Update reference parametersStotal = $state->total:sLasckecorold - ostace,lasckecorold,private function shouldStopPagination(PaginationState $state, int SteamId): bool{...}orivate function handlePaqinationStrateavPaginationState $state,int SresultsPerPaqe.int Steamiid): array 1..,private function shouldSwitchToKeysetPagination(PaginationState $state, int SresultsPerPage): boolt...}private function validateTokenIfNeeded(Client Sclient, PaginationState $state): void{...}private function executeSearchRequest(Client Sclient, string $endpoint, array $payload, PaginationState $state): array{...}private function updateLastRecordId(array $page, PaginationState $state): void{...}private function getNext0ffset(array Spage): intf...}private function loqPaginationProqress(PaginationState Sstate, int SteamId, string Sendpoint): void{...}orivate function calculateDelavInMicroseconds@: intf.}Puched 1 commit to.oriain/IV.20902-undate activitv-ctaae-on-onnortunitvcchande /| View null requect (16 minutes aao)101103— 104107108-111= custom.logscratch &.ison4 SF [jiminny@localhost]« HS_local [jiminny@localhost]& console lPKoD.©ImportBotRecordingJob.php© FixActivitiesOpportunity.php X| ©Opportunity.phpA console [STAGING]class FixActivities0pportunity extends Commandprivate function processParticipants(: vold 4steam = sactivity->qetleamo*suser = sactivity->qetuserosoarticinants= sactivity->oetparticzoantsomforeach ($participants as $participant) <if ($participant->getUserId() |== null || $participant->getEmailAddress() === null) {continue:SemailAddness = Snanticinant->ae+FmailAddnecsOrif (SemailHelper->isCompanyEmail($team, $emailAddress)) {continue.cryfSopportunity = $this->findOpportunityInCrm($crmService, SemailAddress, Suser->getIdO):} catch (Throwable Se) {Sthis->error( string: 'Could not find opportunity: ! . Se->qetMessageO):40kell100% L2• Thu 14 May 16:15:41AskJiminnyReportActivityServiceTest v& console [EUliii accounts [EU]iti stages (EU]Ui teams [EU]12. Vif ($opportunity === null) {Sthis->resetActivity0pportunitv(Sactivitv):124126128154sactivitv-›>uodated'ooportunitv id' => Sooportunity->aetidoio:$this->info( string: 'Opportunity updated for activity: ' • Sactivity->getidO):Tusageprivate function find0pportunityInCrm(ServiceInterface $crmService, string SemailAddress, int $userId): ?0pportunity{...}2 usagesprivate function resetActivityOpportunity(Activity Sactivity): void{...}W Windsurf Teams 99:50 UTF-8 Po 4 spaces...
|
NULL
|
6316287534791890857
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormVIewINavicarecodeLaravelKeractorWindowFV f PhpStormVIewINavicarecodeLaravelKeractorWindowFV faVsco.js#12077 on JY-20903-update_activity-stage-on...haProiect v) CheckAndRetryRemoteMatch.phpC) DeleteForCoachecComActvitesmatchermcommano.pnp© service.ong© Client.php© HubspotPaginationService.php x© MatchActivityCrmData.phpUpoatecrmbata.ongDownloaamissing lrackc rixAcuivitiesopportunit©) HaraDeleteActivitiesto© HardDeleteActivitiesTeclass nuospocracinatlonserviceB12^c) hyoratecalwincrmba© HydrateDefaultActivitviC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities( 78C) MonitorMeetingcounte© MonitorMeetingEndCorC) MonitorMeetinostartco(t) MonitorMeetinaTrait.orC) NotifvNotLoaded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.ptc) ProheMediaSeamentso@ Peascian TranccrintCon"C) ReindeyRecentActivitie: 100)© RetryProspectSummary 103(C) SetProviderCanabilitios 107(e) StatusCount.php© SyncMissingCallDisposi 104© UpdateActivityElasticSe 103© UpdateElasticSearch.pr 132• _ Analvucslaeellonaereyalermnl> C Hubspot> C IntegrationApp> D Traits© AddLavoutEntities.php© AutoloaDelavedComma 160(C) Backfill@pportunitvUsel 198C BullhornCommandAbstC) BullhornPinaCommand, 199© BullhornSearchCommar 209C) BullhornSessioncomma@ CheckActivitvl.oagablei 206C) CleanDunlicateSieldDat 210.©FullSyncOpportunityCoC) 1oaActivitiecCommand. 21g© ManageSyncStrategyCi 223C) MatchCrm@hiectsComr@ MatchOnnortunituAativ© MigrateProvider.php© ProcessHubspotObject: 229(e) DuracDelotodOnnortunOUDLLCoecracnaceavacabeneracor'total_requests' => $state->requestCount,itotal records fetched' => Sstate->totalRecords.'total_elapsed_seconds' => round(Sstate->getElapsedSeconds(, precision: 2),'average_seconds_per_request' => $state->requestCount > 0 ? round( num: Sstate->getElapsedSeconds / $state->requestCount, precision: 2) : 0,]):XlI to Cascade, xI to Command// Update reference parametersStotal = $state->total:sLasckecorold - ostace,lasckecorold,private function shouldStopPagination(PaginationState $state, int SteamId): bool{...}orivate function handlePaqinationStrateavPaginationState $state,int SresultsPerPaqe.int Steamiid): array 1..,private function shouldSwitchToKeysetPagination(PaginationState $state, int SresultsPerPage): boolt...}private function validateTokenIfNeeded(Client Sclient, PaginationState $state): void{...}private function executeSearchRequest(Client Sclient, string $endpoint, array $payload, PaginationState $state): array{...}private function updateLastRecordId(array $page, PaginationState $state): void{...}private function getNext0ffset(array Spage): intf...}private function loqPaginationProqress(PaginationState Sstate, int SteamId, string Sendpoint): void{...}orivate function calculateDelavInMicroseconds@: intf.}Puched 1 commit to.oriain/IV.20902-undate activitv-ctaae-on-onnortunitvcchande /| View null requect (16 minutes aao)101103— 104107108-111= custom.logscratch &.ison4 SF [jiminny@localhost]« HS_local [jiminny@localhost]& console lPKoD.©ImportBotRecordingJob.php© FixActivitiesOpportunity.php X| ©Opportunity.phpA console [STAGING]class FixActivities0pportunity extends Commandprivate function processParticipants(: vold 4steam = sactivity->qetleamo*suser = sactivity->qetuserosoarticinants= sactivity->oetparticzoantsomforeach ($participants as $participant) <if ($participant->getUserId() |== null || $participant->getEmailAddress() === null) {continue:SemailAddness = Snanticinant->ae+FmailAddnecsOrif (SemailHelper->isCompanyEmail($team, $emailAddress)) {continue.cryfSopportunity = $this->findOpportunityInCrm($crmService, SemailAddress, Suser->getIdO):} catch (Throwable Se) {Sthis->error( string: 'Could not find opportunity: ! . Se->qetMessageO):40kell100% L2• Thu 14 May 16:15:41AskJiminnyReportActivityServiceTest v& console [EUliii accounts [EU]iti stages (EU]Ui teams [EU]12. Vif ($opportunity === null) {Sthis->resetActivity0pportunitv(Sactivitv):124126128154sactivitv-›>uodated'ooportunitv id' => Sooportunity->aetidoio:$this->info( string: 'Opportunity updated for activity: ' • Sactivity->getidO):Tusageprivate function find0pportunityInCrm(ServiceInterface $crmService, string SemailAddress, int $userId): ?0pportunity{...}2 usagesprivate function resetActivityOpportunity(Activity Sactivity): void{...}W Windsurf Teams 99:50 UTF-8 Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43785
|
1595
|
12
|
2026-05-14T13:15:42.087011+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764542087_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(all14100% <78• Thu 14 May 16:15:41ec2-user@ip-10-30-145-148:~181DOCKERDEV (-zsh)82APP (-zsh)83-zsh84screenpipe"₴[EMAIL]/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414cec2-user@ip-10-20-…."docker-php-entrypoi..."ec2-user@ip-10-30-.*86 hoursagoUp6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp6 hours(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd889378006ad3430275ebamazon/amazon-ecs-agent: latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec2-user@ip-10-30-145-148 ~]$ docker exec -tidockerexec-ti f3c21482c23f /bin/bash -c "cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"docker exec -tidockereXeс-ti f3c21482c23f /bin/bash -c"cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"Error response from daemon: No suchcontainer: docker[ec2-user@ip-10-30-145-148 ~]$ docker psCONTAINER IDIMAGECOMMANDCREATEDSTATUSPORTSNAMESf3c21482c23ftes (healthy)055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi.."10 minutesagoUp 10 minu9000/tcpecs-worker-conferences-422-worker-conferences-e8cceec2ddffcbc31b00bc68a3f31570055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."tes (healthy)9000/tcpecs-worker-softphone-422-worker-softphone-eedfb1faf8d3ebe1aa0116 minutesagoUp 16 minuee859e8ab6b8055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."58 minutesagoUp 58 minutes (healthy)9000/tcpecs-worker-calendar-421-worker-calendar-b2b9ade8c08bd5ee520027cf2b644193055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."2 hours agoUp 2 hours(healthy)9000/tcpecs-worker-emails-421-worker-emails-f6bac583b7dbc1a01f00106706b6d1ac055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)a8fe3893c15b9000/tcpecs-worker-processing-3-421-worker-processing-3-fcdadbbbe0d788ea8201055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd88937800"docker-php-entrypoi..."6 hours agoUp 6 hours6ad3430275ebamazon/amazon-ecs-agent:latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec?-user@ip-10-30-145-148 ~]$ dockerexec -it $(docker ps --format "{{.ID}}" --filter "name=ecs-worker" | head -1) /bin/bash -c "cd /home/jiminny && bash"root@f3c21482c23f:/home/jiminny# php artisan crm:sync-opportunity--teamId=1052 --from='2022-01-01 00:00:00'--strategy lastModified[2026-05-14 13:15:17] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command" : "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryPeakBeforeCommandInMb":118.0} {"correlation_id":"75555791-00b0-4230-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}Syncing opportunity for Hubspot QAi[2026-05-14 13:15:18] qai.WARNING: [HubSpot] Account not connected for user {"userId": "682978a5-ee72-48b4-86ee-b8efdfc98cd7", "account" : {"Jiminny| \Models||SocialAccount" : {"id" :166,"sociable_id":23552,"provider_user_id":"45562061"., "expires":1778474185, "refresh_token_expires":null,"provider": "hubspot",,"state": "full-refresh", "auth_scope":null, "retry_after" : nul1, "created_at": "2026-03-20 07:38:26", "updated_at": "2026-05-11 04:33:30", "sociable_type": "user"3}} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id" :"5bb63d01-98a5-4а31-8833-e7891c8e4570"}Your HubSpotaccount has become disconnected. Please login to Jiminny to reconnect. skipping..[2026-05-14 13:15:18] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command": "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryAfterCommandInMB" :122.0, "memoryPeakBeforeCommandInMb" : 118.0, "memoryPeakAfterCommandInMB":122.0} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}root@f3c21482c23f:/home/jiminny#l...
|
NULL
|
-8868608837970332015
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(all14100% <78• Thu 14 May 16:15:41ec2-user@ip-10-30-145-148:~181DOCKERDEV (-zsh)82APP (-zsh)83-zsh84screenpipe"₴[EMAIL]/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414cec2-user@ip-10-20-…."docker-php-entrypoi..."ec2-user@ip-10-30-.*86 hoursagoUp6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp6 hours(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd889378006ad3430275ebamazon/amazon-ecs-agent: latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec2-user@ip-10-30-145-148 ~]$ docker exec -tidockerexec-ti f3c21482c23f /bin/bash -c "cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"docker exec -tidockereXeс-ti f3c21482c23f /bin/bash -c"cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"Error response from daemon: No suchcontainer: docker[ec2-user@ip-10-30-145-148 ~]$ docker psCONTAINER IDIMAGECOMMANDCREATEDSTATUSPORTSNAMESf3c21482c23ftes (healthy)055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi.."10 minutesagoUp 10 minu9000/tcpecs-worker-conferences-422-worker-conferences-e8cceec2ddffcbc31b00bc68a3f31570055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."tes (healthy)9000/tcpecs-worker-softphone-422-worker-softphone-eedfb1faf8d3ebe1aa0116 minutesagoUp 16 minuee859e8ab6b8055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."58 minutesagoUp 58 minutes (healthy)9000/tcpecs-worker-calendar-421-worker-calendar-b2b9ade8c08bd5ee520027cf2b644193055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."2 hours agoUp 2 hours(healthy)9000/tcpecs-worker-emails-421-worker-emails-f6bac583b7dbc1a01f00106706b6d1ac055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)a8fe3893c15b9000/tcpecs-worker-processing-3-421-worker-processing-3-fcdadbbbe0d788ea8201055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd88937800"docker-php-entrypoi..."6 hours agoUp 6 hours6ad3430275ebamazon/amazon-ecs-agent:latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec?-user@ip-10-30-145-148 ~]$ dockerexec -it $(docker ps --format "{{.ID}}" --filter "name=ecs-worker" | head -1) /bin/bash -c "cd /home/jiminny && bash"root@f3c21482c23f:/home/jiminny# php artisan crm:sync-opportunity--teamId=1052 --from='2022-01-01 00:00:00'--strategy lastModified[2026-05-14 13:15:17] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command" : "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryPeakBeforeCommandInMb":118.0} {"correlation_id":"75555791-00b0-4230-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}Syncing opportunity for Hubspot QAi[2026-05-14 13:15:18] qai.WARNING: [HubSpot] Account not connected for user {"userId": "682978a5-ee72-48b4-86ee-b8efdfc98cd7", "account" : {"Jiminny| \Models||SocialAccount" : {"id" :166,"sociable_id":23552,"provider_user_id":"45562061"., "expires":1778474185, "refresh_token_expires":null,"provider": "hubspot",,"state": "full-refresh", "auth_scope":null, "retry_after" : nul1, "created_at": "2026-03-20 07:38:26", "updated_at": "2026-05-11 04:33:30", "sociable_type": "user"3}} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id" :"5bb63d01-98a5-4а31-8833-e7891c8e4570"}Your HubSpotaccount has become disconnected. Please login to Jiminny to reconnect. skipping..[2026-05-14 13:15:18] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command": "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryAfterCommandInMB" :122.0, "memoryPeakBeforeCommandInMb" : 118.0, "memoryPeakAfterCommandInMB":122.0} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}root@f3c21482c23f:/home/jiminny#l...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43784
|
1596
|
9
|
2026-05-14T13:15:36.890117+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764536890_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.50265956,"top":0.12529927,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.123703115,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.9644282,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43783
|
NULL
|
NULL
|
NULL
|
|
43783
|
1596
|
8
|
2026-05-14T13:15:33.800188+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764533800_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
0(5)PhostormcodeFV faVsco Project: faVsco.js, menu
0(5)PhostormcodeFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...hroledeyCheckAndRetryRemoteMatch.phpC) DeleteForCoachecComClientonpDownloaamissing lrackc rixAcuivitiesopportunitC) HaraDeleteActivitiestol© Matchermobiect.ong© HardDeleteActivitiesTem A12 ^ Vc) hyoratecalwincrmba© HydrateDefaultActivitviC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(C) MonitorMeetingcounte© MonitorMeetingEndCorC) MonitorMeetinostartco(t) MonitorMeetinaTrait.ohC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentseC) RoaccianTranscrintCom© ReindexRecentActivitie© RetryProspectsummary 106© SetProviderCapabilitiesc) StatusCount.ono© SyncMissingCallDisposi 10%© UpdateActivityElasticS€ 104© UpdateElasticSearch.pl• _ Analvucsw Calendars> C Hubspot• _ IntegrationApp> D Traitsc) AddLavoutEntities.ono• AutoloaDelavedComma© BackfillOpportunityUsel .o© BullhornCommandAbsti 100C) BullhornPinaCommand.© BullhornSearchCommar 199© BullhornSessionComma 205CheckActivityLoggable© CleanDuplicateFieldDat©FullSyncOpportunityColC) 1oaActivitiesCommand©ManageSyncStrategyCrC) MatchCrm@hiectsComr@ MatchOnnortunituAativ@) MiarateProvider.php© ProcessHubspotObject: 22g(e) DuracDelotodOnnortunclass HubspotPaqnationservicepublic function getPaginatedDataGenerator('total_records_fetched' => $state->totalRecords,'total_elapsed_seconds' => round(Sstate->getElapsedSecondsO,precision: 2),'average_seconds_per_request' => $state->requestCount > 0 ? round( num: $state->getElapsedSeconds@D):// Update reference parametersScotal = sscace->cotal:sLasckecorala = sscate-›lasckecorala105106private function shouldStopPagination(PaginationState $state, int SteamId): boolf...;orivate funct.ion handlePaqinationStrateav.array Soavload.array SdefaultFilter,PaginationState $state,int SresultsPerPage,int SteamId): array 1...}private function shouldSwitchToKeysetPagination(PaginationState $state, int SresultsPerPage): boolt...}private function validateTokenIfNeeded(Client Sclient, PaginationState $state): void{...}private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $s 124private function updateLastRecordId(array Spage. PaginationState Sstate): void{...}private function getNext0ffset(array Spage): intf...}|14$private function logPaginationProgress(PaginationState Sstate. int Steamld. strina Sendpoint): void{...}orivate function calculateDelavInMicroseconds@• intf...?unitv-chande /| View null requect (16 minutes ado)= custom.log= laravel.logA SF jiminny@localhost]& ho_local Uiminny@localnost& console [PROD]A console (EU]tiò accounts [EU]fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.org© Activity.php& console SlAGiNGclass rixacciviclesupoorcunlcy excenas commanaorivace tunccion processrarclcloants10 1team = Sactivity->qetleamoruser = sactivitv-›qetuserornarticinants = sactvitv->oetParticioantsooreach ($participants as $participant) <if Soarticioant->aetUserido == null Soarticioant->getema1Address@ === nuub<continueSemailAddness = Snanticinant->ae+Fmai1Addnesso•if (SemailHelper->isCompanyEmail($team, $emailAddress)) {continue.trySopportunity = $this->find0pportunityInCrm(ScrmService, $emailAddress, Suser->getIdO);} catch (Throwable Se) {Sthis->error( string: "Could not find opportunity: ! . Se->qetMessageO) =Sopportunity = null:if (Sopportunity === null) {Sthis->resetActivitv0pportunitv(Sactivity):B2^kell100% 2• Thu 14 May 16:15:33U AskJiminnyReportActivityServiceTest vcascadePipedrive SDK EvaluaActivity Stage DiscrerImport CRM Activity TManual Testing Hubs!+0 ..cmpyalobsets/ ypol/searcht: :Thought for 1sI'll search for where this function is defined and identify the flows that need testingSactivity-›uodated'ooportunity id' => Sopportunity->cetidoio:Sthis->info( string: 'Opportunity updated for activity: '. Sactivity->getidO):› te function findOpportunityInCrm(ServiceInterface $crmService, string SemailAddress, int SuserId): ?0pportunity(...}te function resetActivity0pportunity(Activity Sactivity): void{...}Lookina at the qetPaqinatedDataGenerator function and the existing test file, here are the flows that need testina:Currently Covered Flows v1. Sinale page pagination - No next page2. Multiple pages pagination - 3+ pages with offset3. Emoty results - First reauest returns emoty4. 401 exception with successful retry - Token refresh works5. 401 exception with failed retry - Token refresh tails, exception thrown6. Non-401 exception - Rate limit or other errors thrown directly7. Basic token validation - Method exists (but won't trigger in fast tests)Partialliv Covered Flows8. Safetv limit - Onlv tests constants, not actual limit hit (1000 reguests)9. Keyset pagination - Only tests constants, not actual switch to keyset (after 10,000 records)10. Progress logging - Onlv tests constants, not actual logaina at intervals (every 10 requests)Missing Flows11. shouldStooPacination via satery limit - Actual nit or 1000 request limit with warning 1oa12. Actual kevset nacination switch - When offset + recultsPerDace > 10.00013. Keyset pagination without lastRecordid - Warning logged when can't switch14. Actual progress logging - Logging at request 10, 20, 30, etc15. Token validation timing - When 30+ seconds elapsed since last check16. Missina 'total' in resnonse - Soagef'total'1 22 9 allback17. Missing 'results' in response - Spage ('results'] ?? [] fallback18. Missina 'paging' in response - getNextOffseto handles missina pagina19. Null lastRecordid in results - updateLastRecordId() with null ID20. Delay rate limiting - usleeno actually called hard to test without slowina tests21. Different object types - Currently only tests 'contacts' and 'deals'22. Custom offset parameter - Non-zero startina offset22 Peference narameter undatoc - Verify Stotal and clactPocordtd nronerly undatedCritical Missing Flows to AddA Your included weekly USiquota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00AL-AcAhentrtednctcAaaAsk anvthina (84L)« Code SWF-1.6WN Windsurf Teams00•50UTE.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43782
|
1595
|
11
|
2026-05-14T13:15:33.089423+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764533089_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(all14100% <78• Thu 14 May 16:15:33ec2-user@ip-10-30-145-148:~DOCKERDEV (-zsh)82APP (-zsh)83-zsh84screenpipe"[EMAIL]/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414cec2-user@ip-10-20-…."docker-php-entrypoi..."ec2-user@ip-10-30-.*86 hoursagoUp6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi.."6 hours agoUp6 hours(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd889378006ad3430275ebamazon/amazon-ecs-agent: latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec2-user@ip-10-30-145-148 ~]$ docker exec -tidockerexec-ti f3c21482c23f /bin/bash -c "cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"docker exec -tidockereXeс-ti f3c21482c23f /bin/bash -c"cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"Error response from daemon: No suchcontainer: docker[ec2-user@ip-10-30-145-148 ~]$ docker psCONTAINER IDIMAGECOMMANDCREATEDSTATUSPORTSNAMESf3c21482c23ftes (healthy)055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi.."10 minutesagoUp 10 minu9000/tcpecs-worker-conferences-422-worker-conferences-e8cceec2ddffcbc31b00bc68a3f31570055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."tes (healthy)9000/tcpecs-worker-softphone-422-worker-softphone-eedfb1faf8d3ebe1aa0116 minutesagoUp 16 minuee859e8ab6b8055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."58 minutesagoUp 58 minutes (healthy)9000/tcpecs-worker-calendar-421-worker-calendar-b2b9ade8c08bd5ee520027cf2b644193055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."2 hours agoUp 2 hours(healthy)9000/tcpecs-worker-emails-421-worker-emails-f6bac583b7dbc1a01f00106706b6d1ac055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)a8fe3893c15b9000/tcpecs-worker-processing-3-421-worker-processing-3-fcdadbbbe0d788ea8201055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd88937800"docker-php-entrypoi..."6 hours agoUp 6 hours6ad3430275ebamazon/amazon-ecs-agent:latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec?-user@ip-10-30-145-148 ~]$ dockerexec -it $(docker ps --format "{{.ID}}" --filter "name=ecs-worker" | head -1) /bin/bash -c "cd /home/jiminny && bash"root@f3c21482c23f:/home/jiminny# php artisan crm:sync-opportunity--teamId=1052 --from='2022-01-01 00:00:00'--strategy lastModified[2026-05-14 13:15:17] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command" : "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryPeakBeforeCommandInMb":118.0} {"correlation_id":"75555791-00b0-4230-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}Syncing opportunity for Hubspot QAi[2026-05-14 13:15:18] qai.WARNING: [HubSpot] Account not connected for user {"userId": "682978a5-ee72-48b4-86ee-b8efdfc98cd7", "account" : {"Jiminny| \Models||SocialAccount" : {"id" :166,"sociable_id":23552,"provider_user_id":"45562061"., "expires":1778474185, "refresh_token_expires":null,"provider": "hubspot",,"state": "full-refresh", "auth_scope":null, "retry_after" : nul1, "created_at": "2026-03-20 07:38:26", "updated_at": "2026-05-11 04:33:30", "sociable_type": "user"3}} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id" :"5bb63d01-98a5-4а31-8833-e7891c8e4570"}Your HubSpotaccount has become disconnected. Please login to Jiminny to reconnect. skipping..[2026-05-14 13:15:18] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command": "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryAfterCommandInMB" :122.0, "memoryPeakBeforeCommandInMb" : 118.0, "memoryPeakAfterCommandInMB":122.0} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}root@f3c21482c23f:/home/jiminny#l...
|
NULL
|
6282600175073489046
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(all14100% <78• Thu 14 May 16:15:33ec2-user@ip-10-30-145-148:~DOCKERDEV (-zsh)82APP (-zsh)83-zsh84screenpipe"[EMAIL]/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414cec2-user@ip-10-20-…."docker-php-entrypoi..."ec2-user@ip-10-30-.*86 hoursagoUp6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi.."6 hours agoUp6 hours(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd889378006ad3430275ebamazon/amazon-ecs-agent: latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec2-user@ip-10-30-145-148 ~]$ docker exec -tidockerexec-ti f3c21482c23f /bin/bash -c "cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"docker exec -tidockereXeс-ti f3c21482c23f /bin/bash -c"cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"Error response from daemon: No suchcontainer: docker[ec2-user@ip-10-30-145-148 ~]$ docker psCONTAINER IDIMAGECOMMANDCREATEDSTATUSPORTSNAMESf3c21482c23ftes (healthy)055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi.."10 minutesagoUp 10 minu9000/tcpecs-worker-conferences-422-worker-conferences-e8cceec2ddffcbc31b00bc68a3f31570055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."tes (healthy)9000/tcpecs-worker-softphone-422-worker-softphone-eedfb1faf8d3ebe1aa0116 minutesagoUp 16 minuee859e8ab6b8055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."58 minutesagoUp 58 minutes (healthy)9000/tcpecs-worker-calendar-421-worker-calendar-b2b9ade8c08bd5ee520027cf2b644193055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."2 hours agoUp 2 hours(healthy)9000/tcpecs-worker-emails-421-worker-emails-f6bac583b7dbc1a01f00106706b6d1ac055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)a8fe3893c15b9000/tcpecs-worker-processing-3-421-worker-processing-3-fcdadbbbe0d788ea8201055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd88937800"docker-php-entrypoi..."6 hours agoUp 6 hours6ad3430275ebamazon/amazon-ecs-agent:latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec?-user@ip-10-30-145-148 ~]$ dockerexec -it $(docker ps --format "{{.ID}}" --filter "name=ecs-worker" | head -1) /bin/bash -c "cd /home/jiminny && bash"root@f3c21482c23f:/home/jiminny# php artisan crm:sync-opportunity--teamId=1052 --from='2022-01-01 00:00:00'--strategy lastModified[2026-05-14 13:15:17] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command" : "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryPeakBeforeCommandInMb":118.0} {"correlation_id":"75555791-00b0-4230-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}Syncing opportunity for Hubspot QAi[2026-05-14 13:15:18] qai.WARNING: [HubSpot] Account not connected for user {"userId": "682978a5-ee72-48b4-86ee-b8efdfc98cd7", "account" : {"Jiminny| \Models||SocialAccount" : {"id" :166,"sociable_id":23552,"provider_user_id":"45562061"., "expires":1778474185, "refresh_token_expires":null,"provider": "hubspot",,"state": "full-refresh", "auth_scope":null, "retry_after" : nul1, "created_at": "2026-03-20 07:38:26", "updated_at": "2026-05-11 04:33:30", "sociable_type": "user"3}} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id" :"5bb63d01-98a5-4а31-8833-e7891c8e4570"}Your HubSpotaccount has become disconnected. Please login to Jiminny to reconnect. skipping..[2026-05-14 13:15:18] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command": "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryAfterCommandInMB" :122.0, "memoryPeakBeforeCommandInMb" : 118.0, "memoryPeakAfterCommandInMB":122.0} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}root@f3c21482c23f:/home/jiminny#l...
|
43779
|
NULL
|
NULL
|
NULL
|
|
43781
|
1596
|
7
|
2026-05-14T13:15:31.757786+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764531757_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43780
|
NULL
|
NULL
|
NULL
|
|
43780
|
1596
|
6
|
2026-05-14T13:15:27.551064+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764527551_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PnostormFV faVsco.js~#12077 on JY-20903-update_act PnostormFV faVsco.js~#12077 on JY-20903-update_activity-roledeyCheckAndRetryRemoteMatch.php> D OpportunitySyncStraterv D Pagination© HubspotPaginationS© PaginationConfig.ph© PaginationState.php› _ Prospectsearchstrateo> D Redisv W ServiceTraits€ OpportunitySyncTraT suncermEntitiestrai© SyncFieldsTrait.phpTWritecrmtrait.php•D Uti sWeohook© BatchSyncCollector.phpC) BatchSvncRedisServicel© Client.php© ClosedDealStagesServiC DealFieldsService.php© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.ph/© HubspotClientinterface 100© HubspotTokenManager© PayloadBuilder.php© RemoteCrmObjectManiResponseNormalize.ph| 104© Service.php© SyncFieldAction.php© SyncRelated ActivityMal© WebhooksyncBatchPro 1zz~ D IntegrationApp> ( Accessors.› DApi• Contio•DDTOD FltersOJobs• D ProspectSearchStrategServiceTraitsC) Dataclient.oho© DecorateActivity.phpC) Loca|Search.nhn |© LocalSearchInterface.pl 210© RemoteSearch.php© Service.phpv Mlisteners© ConvertLeadActivities.f© PurgeLookupCache.phk 224> Metadata• MMiarationCo kematchactviyoncrmoojectbetach.ong© ActivitiesMatchCrmCommand.phpo CllentonpHubspotPaginationservice.pnp© MatchCrmObject.phpclass HubspotPaq1nat1onServicepublic function getPaginatedDataGenerator(A12coral_recoros-recched o> oscace >cocalkecoras.'total_elapsed_seconds' => round($state->getELapsedSeconds(), precision: 2), ]jestCount > 0 ? round( num: $state->getELapsedSeconds()D):// Update reference parametersScotal = sscace->cotal:sLasckecorala = sscate-›lasckecoraloprivate function shouldStopPagination(PaginationState Sstate, int SteamId): boolf...}orivate funct.ion handlePaqinationStrateav.array $payload,array SdefaultFilter,PaginationState $state,int SresultsPerPage,int SteamId): array {...J$1PSPostmanprivate function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): boolf..}private function validateTokenIfNeeded(Client $client, PaginationState $state): voidf...}private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $sprivate function updateLastRecordId(array $page, PaginationState $state): voidf...,private function getNext0ffset(array $page): intf…..private function logPaginationProgress(PaginationState Sstate. int $teamid. string Sendnoint)• voidf...?orivate function calculateDelavInMicroseconds@• intf...?104—105-106107108E laravel.logfii stages (EU]4 SF [jiminny@localhost]fi teams (EU)A console [PROD]ImporbotkecoraingJoo.org© Activity.phpA console (EU]fi accounts [EU]© FixActivitiesOpportunity.php x& console SlAGiNGclass rixacciviclesupoorcunlcy excenas commanaorivace tunccion processrarclcloancs1d1team = $activity->getTeam();user = sactivitv-›qetuseronarticinants =sactivitv->oetParticioantsooreach (Sparticipants as $participant) €if Soarticioant->aetUserido == null Soarticioant->getema1Address@ === nuub<SemailAddress = $participant->getEmailAddress();if (SemailHelper->isCompanyEmail($team, SemailAddress)) {comeanue,A2^→149Sthis->resetActivitv0pportunitv(Sactivity):Sactivity-›uodated'ooportunity id' => Sopportunity->cetidoio:Sthis->info( string: 'Opportunity updated for activity: '. Sactivity->getidO):te function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity(..}te function resetActivity0pportunity(Activity $activity): voidf..,• Inu 14 May 10.10.20U AskJiminnyReportActivityServiceTestCascadePipedrive SDK EvaluaActivity Stage DiscrerImport CRM Activity TManual Testing Hubs!+0 ..Thought for 1sI'll search for where this function is defined and identify the flows that need testing.Searched getPaginatedDataGeneratorin ~iminny/appSearched class HubspotPaginationService in ~/jiminny/appLookina at the qetPaqinatedDataGenerator function and the existing test file, here are the flows that need testinaCurrently Covered Flows V1. Single page pagination - No next page2. Multiple pages pagination - 3+ pages with offset3. Empty results - First request returns empty4. 401 exception with successful retry - Token refresh works401 exception with failed retry - Token refresh tails, exception thrownNon-401 exception - Rate limit or other errors thrown directlyBasic token validation - Method exists (but won't trigger in fast tests)rtiallv Covered Flows!Safetv limit - Oniv tests constants, not actual limit hit (1000 requests)Keyset pagination - Only tests constants, not actual switch to keyset (after 10,000 records)10. Progress logging - Only tests constants, not actual logging at intervals (every 10 requests)Missing Flows X11. shouldStopPagination via safety limit - Actual hit of 1000 request limit with warning log12. Actual keyset pagination switch - When offset + resultsPerPage > 10,00013. Keyset pagination without lastRecordid - Warning logged when can't switch14. Actual progress logging - Logging at request 10, 20, 30, etc.15. Token validation timing - When 30+ seconds elapsed since last check16. Missina 'total' in resnonse - Soagef'total'1 22 9 allback17. Missing 'results' in response - Spagel"results'] 77 IJ fallback18. Missina 'paging' in response - getNextOffseto handles missing paging19. Null lastRecordid in results - updateLastRecordId() with null ID20. Delay rate limiting - usleeno actually called hard to test without slowina tests21. Different object types - Currently only tests 'contacts' and 'deals'22. Custom offset parameter - Non-zero starting offset22 Peference narameter undatoc - Verify Stotal and clactPocordtd nronerly undatediCritical Missing Flows to AddA Yourineluded weekly usage quota is exhausted, Purehase extra usage to continue using premium models. Quota resets May 17,1:00A.-AcAAantacAeJAcA-AenaaetenMintachacT_u.Ask anvthina (84L)< CodeSWE-16Pa 4 sp...
|
NULL
|
-6241053004276967674
|
NULL
|
click
|
ocr
|
NULL
|
PnostormFV faVsco.js~#12077 on JY-20903-update_act PnostormFV faVsco.js~#12077 on JY-20903-update_activity-roledeyCheckAndRetryRemoteMatch.php> D OpportunitySyncStraterv D Pagination© HubspotPaginationS© PaginationConfig.ph© PaginationState.php› _ Prospectsearchstrateo> D Redisv W ServiceTraits€ OpportunitySyncTraT suncermEntitiestrai© SyncFieldsTrait.phpTWritecrmtrait.php•D Uti sWeohook© BatchSyncCollector.phpC) BatchSvncRedisServicel© Client.php© ClosedDealStagesServiC DealFieldsService.php© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.ph/© HubspotClientinterface 100© HubspotTokenManager© PayloadBuilder.php© RemoteCrmObjectManiResponseNormalize.ph| 104© Service.php© SyncFieldAction.php© SyncRelated ActivityMal© WebhooksyncBatchPro 1zz~ D IntegrationApp> ( Accessors.› DApi• Contio•DDTOD FltersOJobs• D ProspectSearchStrategServiceTraitsC) Dataclient.oho© DecorateActivity.phpC) Loca|Search.nhn |© LocalSearchInterface.pl 210© RemoteSearch.php© Service.phpv Mlisteners© ConvertLeadActivities.f© PurgeLookupCache.phk 224> Metadata• MMiarationCo kematchactviyoncrmoojectbetach.ong© ActivitiesMatchCrmCommand.phpo CllentonpHubspotPaginationservice.pnp© MatchCrmObject.phpclass HubspotPaq1nat1onServicepublic function getPaginatedDataGenerator(A12coral_recoros-recched o> oscace >cocalkecoras.'total_elapsed_seconds' => round($state->getELapsedSeconds(), precision: 2), ]jestCount > 0 ? round( num: $state->getELapsedSeconds()D):// Update reference parametersScotal = sscace->cotal:sLasckecorala = sscate-›lasckecoraloprivate function shouldStopPagination(PaginationState Sstate, int SteamId): boolf...}orivate funct.ion handlePaqinationStrateav.array $payload,array SdefaultFilter,PaginationState $state,int SresultsPerPage,int SteamId): array {...J$1PSPostmanprivate function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): boolf..}private function validateTokenIfNeeded(Client $client, PaginationState $state): voidf...}private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $sprivate function updateLastRecordId(array $page, PaginationState $state): voidf...,private function getNext0ffset(array $page): intf…..private function logPaginationProgress(PaginationState Sstate. int $teamid. string Sendnoint)• voidf...?orivate function calculateDelavInMicroseconds@• intf...?104—105-106107108E laravel.logfii stages (EU]4 SF [jiminny@localhost]fi teams (EU)A console [PROD]ImporbotkecoraingJoo.org© Activity.phpA console (EU]fi accounts [EU]© FixActivitiesOpportunity.php x& console SlAGiNGclass rixacciviclesupoorcunlcy excenas commanaorivace tunccion processrarclcloancs1d1team = $activity->getTeam();user = sactivitv-›qetuseronarticinants =sactivitv->oetParticioantsooreach (Sparticipants as $participant) €if Soarticioant->aetUserido == null Soarticioant->getema1Address@ === nuub<SemailAddress = $participant->getEmailAddress();if (SemailHelper->isCompanyEmail($team, SemailAddress)) {comeanue,A2^→149Sthis->resetActivitv0pportunitv(Sactivity):Sactivity-›uodated'ooportunity id' => Sopportunity->cetidoio:Sthis->info( string: 'Opportunity updated for activity: '. Sactivity->getidO):te function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity(..}te function resetActivity0pportunity(Activity $activity): voidf..,• Inu 14 May 10.10.20U AskJiminnyReportActivityServiceTestCascadePipedrive SDK EvaluaActivity Stage DiscrerImport CRM Activity TManual Testing Hubs!+0 ..Thought for 1sI'll search for where this function is defined and identify the flows that need testing.Searched getPaginatedDataGeneratorin ~iminny/appSearched class HubspotPaginationService in ~/jiminny/appLookina at the qetPaqinatedDataGenerator function and the existing test file, here are the flows that need testinaCurrently Covered Flows V1. Single page pagination - No next page2. Multiple pages pagination - 3+ pages with offset3. Empty results - First request returns empty4. 401 exception with successful retry - Token refresh works401 exception with failed retry - Token refresh tails, exception thrownNon-401 exception - Rate limit or other errors thrown directlyBasic token validation - Method exists (but won't trigger in fast tests)rtiallv Covered Flows!Safetv limit - Oniv tests constants, not actual limit hit (1000 requests)Keyset pagination - Only tests constants, not actual switch to keyset (after 10,000 records)10. Progress logging - Only tests constants, not actual logging at intervals (every 10 requests)Missing Flows X11. shouldStopPagination via safety limit - Actual hit of 1000 request limit with warning log12. Actual keyset pagination switch - When offset + resultsPerPage > 10,00013. Keyset pagination without lastRecordid - Warning logged when can't switch14. Actual progress logging - Logging at request 10, 20, 30, etc.15. Token validation timing - When 30+ seconds elapsed since last check16. Missina 'total' in resnonse - Soagef'total'1 22 9 allback17. Missing 'results' in response - Spagel"results'] 77 IJ fallback18. Missina 'paging' in response - getNextOffseto handles missing paging19. Null lastRecordid in results - updateLastRecordId() with null ID20. Delay rate limiting - usleeno actually called hard to test without slowina tests21. Different object types - Currently only tests 'contacts' and 'deals'22. Custom offset parameter - Non-zero starting offset22 Peference narameter undatoc - Verify Stotal and clactPocordtd nronerly undatediCritical Missing Flows to AddA Yourineluded weekly usage quota is exhausted, Purehase extra usage to continue using premium models. Quota resets May 17,1:00A.-AcAAantacAeJAcA-AenaaetenMintachacT_u.Ask anvthina (84L)< CodeSWE-16Pa 4 sp...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43779
|
1595
|
10
|
2026-05-14T13:15:30.572708+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764530572_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43778
|
1595
|
9
|
2026-05-14T13:15:27.540652+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764527540_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelplaal100% C8°Thu 14 May 16:15:27•ec2-user@ip-10-30-145-148:~181DOCKERDEV (-zsh)882APP (-zsh)83-zshX4screenpipe"• [EMAIL]/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414cec2-user@ip-10-20-..."docker-php-entrypoi..."ec2-user@ip-10-30-...*86hoursagoUp6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours ago(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd88937800Up6 hours6ad3430275ebamazon/amazon-ecs-agent: latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec2-user@ip-10-30-145-148 ~]$ docker exec -tidockerexec-ti f3c21482c23f /bin/bash -c "cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"docker exec -tidockerexec -ti f3c21482c23f /bin/bash -c"cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"Error response from daemon: No suchcontainer: docker[ec2-user@ip-10-30-145-148 ~]$ docker psCONTAINER IDIMAGECOMMANDCREATEDSTATUSPORTSNAMESf3c21482c23ftes (healthy)055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi.."10 minutesagoUp 10 minu9000/tcpecs-worker-conferences-422-worker-conferences-e8cceec2ddffcbc31b00bc68a3f31570055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."tes (healthy)9000/tcpecs-worker-softphone-422-worker-softphone-eedfb1faf8d3ebe1aa0116 minutesagoUp 16 minuee859e8ab6b8055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."58 minutesagoUp 58 minutes (healthy)9000/tcpecs-worker-calendar-421-worker-calendar-b2b9ade8c08bd5ee520027cf2b644193055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."2 hours agoUp 2 hours(healthy)9000/tcpecs-worker-emails-421-worker-emails-f6bac583b7dbc1a01f00106706b6d1ac055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)a8fe3893c15b9000/tcpecs-worker-processing-3-421-worker-processing-3-fcdadbbbe0d788ea8201055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd88937800"docker-php-entrypoi..."6 hours agoUp 6 hours6ad3430275ebamazon/amazon-ecs-agent:latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec2-user@ip-10-30-145-148 ~]$ docker exec -it $(dockerps --format "{{.ID}}" --filter "name=ecs-worker" | head -1) /bin/bash -c "cd /home/jiminny && bash"root@f3c21482c23f:/home/jiminny# php artisan crm:sync-opportunity--teamId=1052 --from='2022-01-01 00:00:00'--strategy lastModified[2026-05-14 13:15:17] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command" : "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryPeakBeforeCommandInMb":118.0} {"correlation_id":"75555791-00b0-4230-a508-65aa4a0c2ala", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"'}Syncing opportunity for Hubspot QAi[2026-05-14 13:15:18] qai.WARNING: [HubSpot] Account not connected for user {"userId": "682978a5-ee72-48b4-86ee-b8efdfc98cd7", "account" : {"Jiminny| \Models||SocialAccount" : {"id" :166,"sociable_id":23552, "provider_user_id":"45562061", "expires":1778474185, "refresh_token_expires":null,"provider": "hubspot",,"state": "full-refresh", "auth_scope":null, "retry_after" : nul1, "created_at": "2026-03-20 07:38:26", "updated_at": "2026-05-11 04:33:30", "sociable_type": "user"3}} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id" :"5bb63d01-98a5-4а31-8833-e7891c8e4570"}Your HubSpotaccount has become disconnected. Please login to Jiminny to reconnect. skipping...[2026-05-14 13:15:18Jqai.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command": "crm:sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryAfterCommandInMB" :122.0, "memoryPeakBeforeCommandInMb" : 118.0, "memoryPeakAfterCommandInMB":122.0} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}root@f3c21482c23f:/home/jiminny#...
|
NULL
|
7491136668678095016
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelplaal100% C8°Thu 14 May 16:15:27•ec2-user@ip-10-30-145-148:~181DOCKERDEV (-zsh)882APP (-zsh)83-zshX4screenpipe"• [EMAIL]/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414cec2-user@ip-10-20-..."docker-php-entrypoi..."ec2-user@ip-10-30-...*86hoursagoUp6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours ago(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd88937800Up6 hours6ad3430275ebamazon/amazon-ecs-agent: latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec2-user@ip-10-30-145-148 ~]$ docker exec -tidockerexec-ti f3c21482c23f /bin/bash -c "cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"docker exec -tidockerexec -ti f3c21482c23f /bin/bash -c"cd /home/jiminny && bash" /bin/bash -c "cd /home/jiminny && bash"Error response from daemon: No suchcontainer: docker[ec2-user@ip-10-30-145-148 ~]$ docker psCONTAINER IDIMAGECOMMANDCREATEDSTATUSPORTSNAMESf3c21482c23ftes (healthy)055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi.."10 minutesagoUp 10 minu9000/tcpecs-worker-conferences-422-worker-conferences-e8cceec2ddffcbc31b00bc68a3f31570055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."tes (healthy)9000/tcpecs-worker-softphone-422-worker-softphone-eedfb1faf8d3ebe1aa0116 minutesagoUp 16 minuee859e8ab6b8055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."58 minutesagoUp 58 minutes (healthy)9000/tcpecs-worker-calendar-421-worker-calendar-b2b9ade8c08bd5ee520027cf2b644193055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."2 hours agoUp 2 hours(healthy)9000/tcpecs-worker-emails-421-worker-emails-f6bac583b7dbc1a01f00106706b6d1ac055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)a8fe3893c15b9000/tcpecs-worker-processing-3-421-worker-processing-3-fcdadbbbe0d788ea8201055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c"docker-php-entrypoi..."6 hours agoUp 6 hours(healthy)9000/tcpecs-worker-crm-update-100-worker-crm-update-a09dc8c8fcc489b140009556c7f4cbd4055908660479.dkr.ecr.us-east-2.amazonaws.com/jiminny/app/worker-code:9ed2ab9469c286c1152b3554ba4af66ca969414c(healthy)9000/tcpecs-worker-processing-2-421-worker-processing-2-f489b0c19ecd88937800"docker-php-entrypoi..."6 hours agoUp 6 hours6ad3430275ebamazon/amazon-ecs-agent:latest"/agent"8 hours agoUp 8 hours(healthy)ecs-agent[ec2-user@ip-10-30-145-148 ~]$ docker exec -it $(dockerps --format "{{.ID}}" --filter "name=ecs-worker" | head -1) /bin/bash -c "cd /home/jiminny && bash"root@f3c21482c23f:/home/jiminny# php artisan crm:sync-opportunity--teamId=1052 --from='2022-01-01 00:00:00'--strategy lastModified[2026-05-14 13:15:17] qai.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command" : "crm: sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryPeakBeforeCommandInMb":118.0} {"correlation_id":"75555791-00b0-4230-a508-65aa4a0c2ala", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"'}Syncing opportunity for Hubspot QAi[2026-05-14 13:15:18] qai.WARNING: [HubSpot] Account not connected for user {"userId": "682978a5-ee72-48b4-86ee-b8efdfc98cd7", "account" : {"Jiminny| \Models||SocialAccount" : {"id" :166,"sociable_id":23552, "provider_user_id":"45562061", "expires":1778474185, "refresh_token_expires":null,"provider": "hubspot",,"state": "full-refresh", "auth_scope":null, "retry_after" : nul1, "created_at": "2026-03-20 07:38:26", "updated_at": "2026-05-11 04:33:30", "sociable_type": "user"3}} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id" :"5bb63d01-98a5-4а31-8833-e7891c8e4570"}Your HubSpotaccount has become disconnected. Please login to Jiminny to reconnect. skipping...[2026-05-14 13:15:18Jqai.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command": "crm:sync-opportunity", "memoryBeforeCommandInMb" : 118.0, "memoryAfterCommandInMB" :122.0, "memoryPeakBeforeCommandInMb" : 118.0, "memoryPeakAfterCommandInMB":122.0} {"correlation_id":"[CREDIT_CARD]-a508-65aa4a0c2a1a", "trace_id":"5bb63d01-98a5-4a31-8833-e7891c8e4570"}root@f3c21482c23f:/home/jiminny#...
|
43776
|
NULL
|
NULL
|
NULL
|
|
43739
|
1594
|
25
|
2026-05-14T13:10:50.975350+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764250975_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
PhostormcodeFV faVsco.js# Project: faVsco.js, menu
PhostormcodeFV faVsco.js#12077 on JY-20903-update_activity-stage-on...hroledeyCheckAndRetryRemoteMatch.php> D OpportunitySyncStraterv D Pagination© HubspotPaginationSCo kematchactviyoncrmoojectbetach.ongo Cllentonp©Paginationcontig.on© Matchermobiect.ongm A12 ^ V› _ Prospectsearchstrateo> C Redisv W ServiceTraits• OpportunitysynctraT suncermEntitiestrai@ SvncFieldsTrait.phpTWritecrmtrait.phpWeohookC) BatchSvncCollector.ohrC) BatchSvncRedisServicel© Client.phpC) ClosedDealStadesServi@ DealFieldsService.phpC) DecorateActivitv nhnc) Sielddefinitions.ong©) FieldTvoeConverter.ong© HubspotClientinterface 106c) HubsootTokenmanagel(e) DavloadBuilder.ono(C DomatoßrmOhiontManiResponseNormalize.pni 104© Service.php© SyncFieldAction.php© SyncRelatedActivityMa© WebhooksyncBatchPro 1zzv D IntegrationApr151> ( Accessors.> M Api•_ conticMoTOM.Jobs• D ProspectSearchStrateg• M ServiceTraitsC) Dataclient.ohoC) DecorateActivitv. ohnC) Loca|Search.nhn |© LocalSearchinterface.pl .© RemoteSearch.phpC) Service nhnv Mlisteners© ConvertLeadActivities.r© PurgeLookupCache.php 224• MMiarationclass HubspotPaqinationserv1cepublic function getPaginatedDataGenerator(coral_recoros-recched o> oscace >coratrecoras.'total_elapsed_seconds' => round(Sstate->getElapsedSecondsO,precision: 2),'average_seconds_per_request' => $state->requestCount > 0 ? round( num: $state->getElapsedSeconds©D):// Update reference parametersScotal = sscace->cotal:sLasckecorala = sscate-›lasckecoraloprivate function shouldStopPagination(PaginationState Sstate, int SteamId): boolf...;orivate funct.ion handlePaqinationStrateavarray Soavload.array $defaultFilter,PaginationState $state,int SresultsPerPage,int SteamId): array 1...}private function shouldSwitchToKeysetPagination(PaginationState $state, int SresultsPerPage): boolf...}private function validateTokenIfNeeded(Client Sclient, PaginationState $state): void{...}private function executeSearchRequest(Client Sclient, string $endpoint, array $payload, PaginationState $sprivate function updateLastRecordId(array Spage. PaginationState Sstate): void{...}private function getNext0ffset(array Spage): intf...}private function logPaginationProgress(PaginationState Sstate. int $teamid. string Sendnoint): voidf...?orivate function calculatedelavinMicrosecondso: int....kal100% L2. Thu 14 May 16:10:50AskJiminnyReportActivityServiceTest v105106= custom.log= laravel.logA SF jiminny@localhost]& ho_local Uiminny@localnost& console [PROD]A console (EU]in accounts cu)fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.org© Activity.php& console SlAGiNGclass rixacciviclesupoorcunlcy excenas commanoorivace tunccion processrarclcloants10 1team = Sactivity->qetleamoruser = sactivitv-›qetuserornarticinants = sactvitv->oetParticioantsonoreach Soarticioants as Soarticivant) «if (Sparticipant->getUserId@) |== null II Sparticinant->qetEmailAddress() === null) {continueSemailAddness = Snanticinant->ae+Fmai1Addnesso•if (SemailHelper->isCompanyEmail($team, $emailAddress)) {continue.trySopportunity = $this->findOpportunityInCrm($crmService, $emailAddress, Suser->getIdO):} catch (Throwable Se) {Sthis->error( string: 'Could not find opportunity: • . Se->getMessageO):Sopportunity = null:if (Sopportunity === null) {Sthis->resetActivitv0pportunitv(Sactivity):Cascade82^Sactivity-›uodated'ooportunity id' => Sopportunity->cetidoio:Sthis->info( string: 'Opportunity updated for activity: '. Sactivity->getidO):|14$› te function findOpportunityInCrm(ServiceInterface ScrmService, string SemailAddress, int SuserId): ?0pportunity(...}te function resetActivity0pportunity(Activity Sactivity): void{...}W Windsurf Toamio 4 spaces...
|
43736
|
NULL
|
NULL
|
NULL
|
|
43738
|
1593
|
28
|
2026-05-14T13:10:52.752196+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764252752_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43737
|
1593
|
27
|
2026-05-14T13:10:47.551766+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764247551_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGit•FV faVsco.jsv• #12077 on JY-20903-update_activity-stage-on...hangeProject v:WindowHelpabl19L AskJiminnyReportActivityServiceTest ~100% C 8• Thu 14 May 16:10:47QCascadeOpportunitySyncStrate,InviteUserToTeamAction.phpv D Pagination© UserInvitationDTO.phpcustom.log(° scratch_8.json© HubspotPaginationS© PaginationConfig.ph© CrmActivityService.php=laravel.log© PaginationState.php© CheckAndRetryRemoteMatch.ph| A SF [jiminny@localh18> D ProspectSearchStrateg› D RedisRematchActivityOnCrmObjectDet4 HS_local (jiminny@v D ServiceTraits+ OpportunitySyncTra© ActivitiesMatchCrmCommand.phA console (PROD]T SyncCrmEntitiesTrai© Service.phpA console (EU]+ SyncFieldsTrait.phpClient.phpinb)accounts [EU]© WriteCrmTrait.php© HubspotPaginationService.php((nD)stages [EU]> D Utils> D Webhook© MatchActivityCrmData.phpiii teams [EU]© BatchSyncCollector.php© ImportParticipants.php© ImportBotRecordin© BatchSyncRedisService© Client.php© UpdateCrmData.php© Activity.php© ClosedDealStagesServi© MatchCrmObject.php© FixActivitiesOpporiC DealFieldsService.php© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.phi© HubspotClientinterface© HubspotTokenManager© PayloadBuilder.phpC RemoteCrmObjectMani© ResponseNormalize.php© Service.php© SyncFieldAction.php© SyncRelatedActivityMai© WebhookSyncBatchPro14267374757677787980818283clasA12 ^v'public function'total'totat11 mg'total90'averag114Opportunity.phpA console [STAGINGA2privaC IntegrationAppD Accessors1 usageprivate functio 123• Api8498D Config1):aTI to Cascade 116// Update n,117$total = $s 118$lastRecord 119120121122124125vity);oportunit1 usager activitMATAPushed 1 commit to origin/JY-20903-update_activity-stage-on-opportunity-change // View pull request (11 minutes ago)Pipedrive SDK EvaluatiActivity Stage DiscrepaImport CRM Activity TyManual Testing HubSptoumy apiasinecuruiu - nun): |Generator ($state = new PaginationState(offset: Soffset);Sendpoint = Client::BASE_URL . "/crm/v3/objects/(Stype)/search";SdefaultFilter = $payload ['filters'] ?? 0;SresultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;Steamld = Sclient->getConfig0->getTeam()->getld0):Thought for 1s ›I'll search for where this function is defined and identify the flows that need testing.Searched getPaginatedDataGenerator in ~/jiminny/appSearched class HubspotPaginationService in ~/jiminny/appThoughts >Read HubspotPaginationServiceTest.php and HubspotPaginationService.php >Thought for 3s >Looking at the getPaginatedDataGenerator function and the existing test file, here are the flows that need testing:Currently Covered Flows M1. Single page pagination - No next page2. Multiple pages pagination - 3+ pages with offset3.Empty results - First request returns empty4.401 exception with successful retry - Token refresh works5.401 exception with failed retry - Token refresh fails, exception thrown6. Non-401 exception - Rate limit or other errors thrown directly7. Basic token validation - Method exists (but won't trigger in fast tests)Partially Covered Flows A8.Safety limit - Only tests constants, not actual limit hit (1000 requests)9. Keyset pagination - Only tests constants, not actual switch to keyset (after 10,000 records)10. Progress logging - Only tests constants, not actual logging at intervals (every 10 requests)Missing Flows X11.shouldStopPagination via safety limit - Actual hit of 1000 request limit with warning log12.Actual keyset pagination switch - When offset + resultsPerPage > 10,00013. Keyset pagination without lastRecordid - Warning logged when can't switch14. Actual progress logging - Logging at request 10, 20, 30, etc.WW Windsurf Teams 78:1 (2229 chars, 56 line breaks)UTF-8Co 4 spaces...
|
NULL
|
-7940069861088208681
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGit•FV faVsco.jsv• #12077 on JY-20903-update_activity-stage-on...hangeProject v:WindowHelpabl19L AskJiminnyReportActivityServiceTest ~100% C 8• Thu 14 May 16:10:47QCascadeOpportunitySyncStrate,InviteUserToTeamAction.phpv D Pagination© UserInvitationDTO.phpcustom.log(° scratch_8.json© HubspotPaginationS© PaginationConfig.ph© CrmActivityService.php=laravel.log© PaginationState.php© CheckAndRetryRemoteMatch.ph| A SF [jiminny@localh18> D ProspectSearchStrateg› D RedisRematchActivityOnCrmObjectDet4 HS_local (jiminny@v D ServiceTraits+ OpportunitySyncTra© ActivitiesMatchCrmCommand.phA console (PROD]T SyncCrmEntitiesTrai© Service.phpA console (EU]+ SyncFieldsTrait.phpClient.phpinb)accounts [EU]© WriteCrmTrait.php© HubspotPaginationService.php((nD)stages [EU]> D Utils> D Webhook© MatchActivityCrmData.phpiii teams [EU]© BatchSyncCollector.php© ImportParticipants.php© ImportBotRecordin© BatchSyncRedisService© Client.php© UpdateCrmData.php© Activity.php© ClosedDealStagesServi© MatchCrmObject.php© FixActivitiesOpporiC DealFieldsService.php© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.phi© HubspotClientinterface© HubspotTokenManager© PayloadBuilder.phpC RemoteCrmObjectMani© ResponseNormalize.php© Service.php© SyncFieldAction.php© SyncRelatedActivityMai© WebhookSyncBatchPro14267374757677787980818283clasA12 ^v'public function'total'totat11 mg'total90'averag114Opportunity.phpA console [STAGINGA2privaC IntegrationAppD Accessors1 usageprivate functio 123• Api8498D Config1):aTI to Cascade 116// Update n,117$total = $s 118$lastRecord 119120121122124125vity);oportunit1 usager activitMATAPushed 1 commit to origin/JY-20903-update_activity-stage-on-opportunity-change // View pull request (11 minutes ago)Pipedrive SDK EvaluatiActivity Stage DiscrepaImport CRM Activity TyManual Testing HubSptoumy apiasinecuruiu - nun): |Generator ($state = new PaginationState(offset: Soffset);Sendpoint = Client::BASE_URL . "/crm/v3/objects/(Stype)/search";SdefaultFilter = $payload ['filters'] ?? 0;SresultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;Steamld = Sclient->getConfig0->getTeam()->getld0):Thought for 1s ›I'll search for where this function is defined and identify the flows that need testing.Searched getPaginatedDataGenerator in ~/jiminny/appSearched class HubspotPaginationService in ~/jiminny/appThoughts >Read HubspotPaginationServiceTest.php and HubspotPaginationService.php >Thought for 3s >Looking at the getPaginatedDataGenerator function and the existing test file, here are the flows that need testing:Currently Covered Flows M1. Single page pagination - No next page2. Multiple pages pagination - 3+ pages with offset3.Empty results - First request returns empty4.401 exception with successful retry - Token refresh works5.401 exception with failed retry - Token refresh fails, exception thrown6. Non-401 exception - Rate limit or other errors thrown directly7. Basic token validation - Method exists (but won't trigger in fast tests)Partially Covered Flows A8.Safety limit - Only tests constants, not actual limit hit (1000 requests)9. Keyset pagination - Only tests constants, not actual switch to keyset (after 10,000 records)10. Progress logging - Only tests constants, not actual logging at intervals (every 10 requests)Missing Flows X11.shouldStopPagination via safety limit - Actual hit of 1000 request limit with warning log12.Actual keyset pagination switch - When offset + resultsPerPage > 10,00013. Keyset pagination without lastRecordid - Warning logged when can't switch14. Actual progress logging - Logging at request 10, 20, 30, etc.WW Windsurf Teams 78:1 (2229 chars, 56 line breaks)UTF-8Co 4 spaces...
|
43735
|
NULL
|
NULL
|
NULL
|
|
43736
|
1594
|
24
|
2026-05-14T13:10:45.025044+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764245025_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormProiectINavicarecodeFV faVsco.js( #12077 o PhostormProiectINavicarecodeFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...hakal100% 2. Thu 14 May 16:10:44U AskJiminnyReportActivityServiceTest vCheckAndRetryRemoteMatch.php> D OpportunitySyncStraterv D Pagination© HubspotPaginationS© PaginationConfig.phCo kematchactviyoncrmoojectbetach.ongclientonp= custom.logA SF jiminny@localhost]& ho_local Uiminny@localnost& console [PROD]A console (EU]in accounts cu)fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.org© Activity.phpcascadePipedrive SDK EvaluaActivity Stage DiscrerImport CRM Activity TManual Testing Hubs!+0 ..© FixActivitiesOpportunity.php& console SlAGiNG© Matchermobiect.ongclass rixacciviclesupoorcunlcy excenas commanarunccion processrarclcloancsclass HubspotPaqnat1onServicem A12 ^ Vpublic function getPaginatedDataGenerator(Sthis->updateLastRecordId(Spage, $state);Steam = Sactivity->getTeamO:suser = sactivitv->qetuseronSoarticinants = sactivitv->oetparticioantsorB2^_ Prospectsearchstrateo› D Redisv W ServiceTraitsOpportunitvsyncura(© SyncCrmEntitiesTrai@ SvncFieldsTrait.phpTWritecrmtrait.php→DUusWeohook© BatchSyncCollector.phfC) BatchSvncRedisService© Client.phpC) ClosedDealStadesServil@ DealFieldsService.phpC) DecorateActivitv nho© FieldDefinitions.phpC FieldTvoeConverter.on0 HubsnotClientinterfaco© HubspotTokenManager© PayloadBuilder.php© RemoteCrmObjectMani€ ResponseNormalize.ph© Service.php© SyncFieldAction.php© SyncRelatedActivityMa© WebhookSyncBatchPrcv _ IntearationApu> ( Accessors.> M Api• _ contic• GoтO.a Filter.M.lobs> D ProspectSearchStrateg> MServiceTraitsC) Dataclient.oho@ DecorateActivitv.nhnC)LocalSearch.nhnl101.@ LocalSearchInterface.pl 10%© RemoteSearch.php@ Service nhnv Ml isteners© ConvertLeadActivities.p 1z2e Duraol ookunGache nhr• MMiarationThought for 1sl'll search for where this function is defined and identiry the tiows that need testing.// Safely iterate over results with null checkSearched getPaginatedDataGenerator in ~/iminny/appSresulus = spagel"results' ?? Urforeach (Sresults as Srow) {Sstate->incrementTotalRecordsOforeach (Sparticivants as Soarticioant) <if (Soarticinant->getUserido == null Soarticioant->getemai Address@ z== nuub<icontinueSstate->setoffset(Sthis->getNextuffsetSoage)0:10€- 101103104- 105.106107Sthis->logPaginationProgress(Sstate, Steamid, $endpoint):} while (Sstate->offset && ! empty(Spage['results']));Loo final nagination comoletion stats$this->logger->info('[Hubspot] Pagination completed', [itoam id' => Steamid— 112'total_records_fetched' => $state->totalRecords,itotal plansed secondsl => nound(Sstate->netflansedSecondco'average_seconds_per_request' => $state-›requestCount › 0 ? round( num: Sstate->getElapsedSeconds 118D):xlI to Cascade, &I to Com// Update reference parametersscocal = sstace->coual.SlastRecordId = Sstate->lastRecordId:private function shouldStopPagination(PaginationState $state, int SteamId): boolf...}private function handlePaginationStrateav^array Spavloadlarrav SdefaultFilter.PaginationState $state,int SresultsPerPage,int Steamid): array 1...}/149private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool{...}/cmsobses5/5ypo/search/::Thouaht for 3slLookina at the qetPaqinatedDataGenerator function and the existing test file, here are the flows that need testinaSemailAddnecs = Snanticinant->ae+Fmai1Addness0•if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) 1continue.trySopportunity = $this->find0pportunityInCrm($crmService, SemailAddress, Suser->getIdO):} catch (Throwable $e) {Sthis->error( string: 'Could not find opportunity: • . $e->qetMessage):Currently Covered Flows v1. Sinale page pagination - No next page2. Multiple pages pagination - 3+ pages with offset3. Emoty results - First reauest returns emoty4. 401 exception with successful retry - Token refresh works5. 401 exception with failed retry - Token refresh tails, exception thrown6. Non-401 exception - Rate limit or other errors thrown directly7. Basic token validation - Method exists (but won't trigger in fast tests)if (Sopportunity === null) {Sthis->resetActivity0pportunitv(Sactivitv):Partialliv Covered Flows!8. Safetv limit - Onlv tests constants, not actual limit hit (1000 reguests)9. Keyset pagination - Only tests constants, not actual switch to keyset (after 10,000 records)10. Progress logging - Onlv tests constants, not actual loggina at intervals (every 10 requests)Missing Flows11. shouldStooPacination via satery limit - Actual nit or 1000 request limit with warnina loa12. Actual kevset nacination switch - When offset + recultsPerDace > 10.000.13. Keyset pagination without lastRecordid - Warning logged when can't switchSactivitv->uodatedl 'ooportunity id' => Sooportunity->qetidoi0:14. Actual progress logging - Logging at request 10, 20, 30, etc15. Token validation timing - When 30+ seconds elapsed since last checkSthis->infod string:Onnortunity undated for activitv. !, sactivitv->aetidiobe16. Missina 'total' in resnonse - Soagef'total'1 22 9 allback17. Missing 'results' in response - Spage ('results'] ?? [] fallback18. Missina 'paging' in response - getNextOffseto handles missina paginaTusage19. Null lastRecordid in results - updateLastRecordId() with null IDprivate function findOpportunityInCrm(ServiceInterface $crmService, string SemailAddress, int $userId): ?Opportunity20. Delay rate limiting - usleen" actually called hard to test without slowing tests21. Different object types - Currently only tests 'contacts' and 'deals'Z usages22. Custom offset parameter - Non-zero startina offsetprivate function resetActivityOpportunity(Activity Sactivity): void{...}22 Peference narameter undatoc - Verify Stotal and clactPocordtd nronerly undatodCritical Missing Flows to Add• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00A.-AcAAantacAeJAcA-AenaaetenMintachacT_u.-AMectaadten AleclecteteteAiaaaalAsk anvthina (84L)< CodeSWE-16W Windsurf Teams 78:1 (2229 chars, 56 line breaks)UTE.8io 4 spaces...
|
NULL
|
-4416790931507111452
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormProiectINavicarecodeFV faVsco.js( #12077 o PhostormProiectINavicarecodeFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...hakal100% 2. Thu 14 May 16:10:44U AskJiminnyReportActivityServiceTest vCheckAndRetryRemoteMatch.php> D OpportunitySyncStraterv D Pagination© HubspotPaginationS© PaginationConfig.phCo kematchactviyoncrmoojectbetach.ongclientonp= custom.logA SF jiminny@localhost]& ho_local Uiminny@localnost& console [PROD]A console (EU]in accounts cu)fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.org© Activity.phpcascadePipedrive SDK EvaluaActivity Stage DiscrerImport CRM Activity TManual Testing Hubs!+0 ..© FixActivitiesOpportunity.php& console SlAGiNG© Matchermobiect.ongclass rixacciviclesupoorcunlcy excenas commanarunccion processrarclcloancsclass HubspotPaqnat1onServicem A12 ^ Vpublic function getPaginatedDataGenerator(Sthis->updateLastRecordId(Spage, $state);Steam = Sactivity->getTeamO:suser = sactivitv->qetuseronSoarticinants = sactivitv->oetparticioantsorB2^_ Prospectsearchstrateo› D Redisv W ServiceTraitsOpportunitvsyncura(© SyncCrmEntitiesTrai@ SvncFieldsTrait.phpTWritecrmtrait.php→DUusWeohook© BatchSyncCollector.phfC) BatchSvncRedisService© Client.phpC) ClosedDealStadesServil@ DealFieldsService.phpC) DecorateActivitv nho© FieldDefinitions.phpC FieldTvoeConverter.on0 HubsnotClientinterfaco© HubspotTokenManager© PayloadBuilder.php© RemoteCrmObjectMani€ ResponseNormalize.ph© Service.php© SyncFieldAction.php© SyncRelatedActivityMa© WebhookSyncBatchPrcv _ IntearationApu> ( Accessors.> M Api• _ contic• GoтO.a Filter.M.lobs> D ProspectSearchStrateg> MServiceTraitsC) Dataclient.oho@ DecorateActivitv.nhnC)LocalSearch.nhnl101.@ LocalSearchInterface.pl 10%© RemoteSearch.php@ Service nhnv Ml isteners© ConvertLeadActivities.p 1z2e Duraol ookunGache nhr• MMiarationThought for 1sl'll search for where this function is defined and identiry the tiows that need testing.// Safely iterate over results with null checkSearched getPaginatedDataGenerator in ~/iminny/appSresulus = spagel"results' ?? Urforeach (Sresults as Srow) {Sstate->incrementTotalRecordsOforeach (Sparticivants as Soarticioant) <if (Soarticinant->getUserido == null Soarticioant->getemai Address@ z== nuub<icontinueSstate->setoffset(Sthis->getNextuffsetSoage)0:10€- 101103104- 105.106107Sthis->logPaginationProgress(Sstate, Steamid, $endpoint):} while (Sstate->offset && ! empty(Spage['results']));Loo final nagination comoletion stats$this->logger->info('[Hubspot] Pagination completed', [itoam id' => Steamid— 112'total_records_fetched' => $state->totalRecords,itotal plansed secondsl => nound(Sstate->netflansedSecondco'average_seconds_per_request' => $state-›requestCount › 0 ? round( num: Sstate->getElapsedSeconds 118D):xlI to Cascade, &I to Com// Update reference parametersscocal = sstace->coual.SlastRecordId = Sstate->lastRecordId:private function shouldStopPagination(PaginationState $state, int SteamId): boolf...}private function handlePaginationStrateav^array Spavloadlarrav SdefaultFilter.PaginationState $state,int SresultsPerPage,int Steamid): array 1...}/149private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool{...}/cmsobses5/5ypo/search/::Thouaht for 3slLookina at the qetPaqinatedDataGenerator function and the existing test file, here are the flows that need testinaSemailAddnecs = Snanticinant->ae+Fmai1Addness0•if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) 1continue.trySopportunity = $this->find0pportunityInCrm($crmService, SemailAddress, Suser->getIdO):} catch (Throwable $e) {Sthis->error( string: 'Could not find opportunity: • . $e->qetMessage):Currently Covered Flows v1. Sinale page pagination - No next page2. Multiple pages pagination - 3+ pages with offset3. Emoty results - First reauest returns emoty4. 401 exception with successful retry - Token refresh works5. 401 exception with failed retry - Token refresh tails, exception thrown6. Non-401 exception - Rate limit or other errors thrown directly7. Basic token validation - Method exists (but won't trigger in fast tests)if (Sopportunity === null) {Sthis->resetActivity0pportunitv(Sactivitv):Partialliv Covered Flows!8. Safetv limit - Onlv tests constants, not actual limit hit (1000 reguests)9. Keyset pagination - Only tests constants, not actual switch to keyset (after 10,000 records)10. Progress logging - Onlv tests constants, not actual loggina at intervals (every 10 requests)Missing Flows11. shouldStooPacination via satery limit - Actual nit or 1000 request limit with warnina loa12. Actual kevset nacination switch - When offset + recultsPerDace > 10.000.13. Keyset pagination without lastRecordid - Warning logged when can't switchSactivitv->uodatedl 'ooportunity id' => Sooportunity->qetidoi0:14. Actual progress logging - Logging at request 10, 20, 30, etc15. Token validation timing - When 30+ seconds elapsed since last checkSthis->infod string:Onnortunity undated for activitv. !, sactivitv->aetidiobe16. Missina 'total' in resnonse - Soagef'total'1 22 9 allback17. Missing 'results' in response - Spage ('results'] ?? [] fallback18. Missina 'paging' in response - getNextOffseto handles missina paginaTusage19. Null lastRecordid in results - updateLastRecordId() with null IDprivate function findOpportunityInCrm(ServiceInterface $crmService, string SemailAddress, int $userId): ?Opportunity20. Delay rate limiting - usleen" actually called hard to test without slowing tests21. Different object types - Currently only tests 'contacts' and 'deals'Z usages22. Custom offset parameter - Non-zero startina offsetprivate function resetActivityOpportunity(Activity Sactivity): void{...}22 Peference narameter undatoc - Verify Stotal and clactPocordtd nronerly undatodCritical Missing Flows to Add• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00A.-AcAAantacAeJAcA-AenaaetenMintachacT_u.-AMectaadten AleclecteteteAiaaaalAsk anvthina (84L)< CodeSWE-16W Windsurf Teams 78:1 (2229 chars, 56 line breaks)UTE.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43735
|
1593
|
26
|
2026-05-14T13:10:44.182624+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764244182_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp>0 hl100% CDEV (-zsh)|-zsh8• Thu 14 May 16:10:43181ec2-user@ip-10-20-31-14...X7DOCKERO 81DEV (-zsh)882APP (-zsh)83screenpipe*root@docker_Lamp_1:/home/Jiminny# php artisanactivity:update:es 422003Found activity ID: 422003,UUID: f43cf158-e60d-46e5-92f8-c4e0594a3219Sending activity for ES update...Done.root@docker_lamp_1:/home/jiminny#What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20903-update_activity-stage-on-opportunity-change) $ devfailed to connect to the docker API at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20903-update_activity-stage-on-opportunity-change) $ O...
|
NULL
|
3281884820355466254
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp>0 hl100% CDEV (-zsh)|-zsh8• Thu 14 May 16:10:43181ec2-user@ip-10-20-31-14...X7DOCKERO 81DEV (-zsh)882APP (-zsh)83screenpipe*root@docker_Lamp_1:/home/Jiminny# php artisanactivity:update:es 422003Found activity ID: 422003,UUID: f43cf158-e60d-46e5-92f8-c4e0594a3219Sending activity for ES update...Done.root@docker_lamp_1:/home/jiminny#What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20903-update_activity-stage-on-opportunity-change) $ devfailed to connect to the docker API at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20903-update_activity-stage-on-opportunity-change) $ O...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43734
|
1594
|
23
|
2026-05-14T13:10:41.864931+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764241864_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
CaltActivityFllesJiminny…..~@ jiminny-x-integrati& CaltActivityFllesJiminny…..~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-clinia# curiosity_lab# engineering# general# jiminny-bgac nlatorm-nckets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages8. A...Ae o2C. Vasil Vasilev EGalva DimitrovaFA. Stefka Stoyanova @%: Todor StamatovN. Mario GeorgievP. Nikolay Ivanov2o James Graham PR. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova¿ . Aneliya Angelova •Messagest Add canvasur FilesTuesday. April 28thvаля каза че не се използва фичъра.и няма пооблем ла гоъмнеMonday, May 11th~Aneliya Angelova 1:24 PMЛукаш за Hubspot за синковете вече се използва тази команда нали?crm: sync-hubspot-objectsLukas Kovalik O 1:32 PMла коон я пуска поез 5 минTodayAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук, нали?при останалите CRMi трябва ръчно да се въведатLukas Kovalik 2:47 PMтрябва да се за всички. някьде не се ли полълва$1при зохо май беше hardcoded но май и там си връшаха деAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.auassian.net/prowse/jy-zU/2oJira Cloud -[HubSpot] Optimise CRM rematching on delete hubspot ac...X Bueb5-2012 in im stocRlReady for QA= MediumAA Aneliya AngelovaAs of today at 4:00 PMOpen in Jira+ SummariseLukas Kovalik 4:02 PMзвьни направоYou joined the huddle LIVE 4:06 PMAneliva Ancelova is here tooMessage Aneliva Angelova+ АalY= Al Notes: OffPSPhpStormLukas KovalikAl Notes: OffLeave→6 Huddle with Aneliya Angelova• Inu 14 Mау 10.10.47lobalLeave...
|
NULL
|
519093569925713928
|
NULL
|
visual_change
|
ocr
|
NULL
|
CaltActivityFllesJiminny…..~@ jiminny-x-integrati& CaltActivityFllesJiminny…..~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-clinia# curiosity_lab# engineering# general# jiminny-bgac nlatorm-nckets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages8. A...Ae o2C. Vasil Vasilev EGalva DimitrovaFA. Stefka Stoyanova @%: Todor StamatovN. Mario GeorgievP. Nikolay Ivanov2o James Graham PR. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova¿ . Aneliya Angelova •Messagest Add canvasur FilesTuesday. April 28thvаля каза че не се използва фичъра.и няма пооблем ла гоъмнеMonday, May 11th~Aneliya Angelova 1:24 PMЛукаш за Hubspot за синковете вече се използва тази команда нали?crm: sync-hubspot-objectsLukas Kovalik O 1:32 PMла коон я пуска поез 5 минTodayAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук, нали?при останалите CRMi трябва ръчно да се въведатLukas Kovalik 2:47 PMтрябва да се за всички. някьде не се ли полълва$1при зохо май беше hardcoded но май и там си връшаха деAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.auassian.net/prowse/jy-zU/2oJira Cloud -[HubSpot] Optimise CRM rematching on delete hubspot ac...X Bueb5-2012 in im stocRlReady for QA= MediumAA Aneliya AngelovaAs of today at 4:00 PMOpen in Jira+ SummariseLukas Kovalik 4:02 PMзвьни направоYou joined the huddle LIVE 4:06 PMAneliva Ancelova is here tooMessage Aneliva Angelova+ АalY= Al Notes: OffPSPhpStormLukas KovalikAl Notes: OffLeave→6 Huddle with Aneliya Angelova• Inu 14 Mау 10.10.47lobalLeave...
|
43731
|
NULL
|
NULL
|
NULL
|
|
43733
|
1593
|
25
|
2026-05-14T13:10:42.382842+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764242382_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43732
|
NULL
|
NULL
|
NULL
|
|
43686
|
1594
|
0
|
2026-05-14T13:07:59.093194+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764079093_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43685
|
1593
|
0
|
2026-05-14T13:07:58.228068+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764078228_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-9167151174974584710
|
-8204148901454697532
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
SlackFileEditViewGoHistoryWindowHelp• 0DEV (-zsh)DOCKERO 881DEV (-zsh)₴2tests/Unit/Policies/CanAccessAiReportsTest.phpAPP (-zsh)83-zshscreenpipe*nochanges added to commit (use "gitadd"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20903-update_activity-stage-on-opportunity-change) $ devroot@docker_lamp_1:/home/jiminny# php artisan activity:update:es 422003Found activity ID: 16,UUID: 988920ba-cdf8-43bc-a869-1f6d85e55fa0Sending activity for ES update..Done.root@docker_lamp_1:/home/jiminny#php artisan activity:update:es 422003Foundactivity ID: 422003,UUID: f43cf158-e60d-46e5-92f8-c4e0594a3219Sending activity for ES update..Done.root@docker_lamp_1:/home/jiminny#php artisan activity:update:es 422003Foundactivity ID: 422003,UUID: f43cf158-e60d-46e5-92f8-c4e0594a3219Sending activity for ESupdate.Done.root@docker_lamp_1:/home/jiminny#What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20903-update_activity-stage-an epportunity-change) $ l>0 hl100% <78• Thu 14 May 16:07:58181• ₴5ec2-user@ip-10-30-129-.. *6ec2-user@ip-10-20-31-14... 87DEV...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43663
|
1591
|
20
|
2026-05-14T13:06:45.645541+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778764005645_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43657
|
NULL
|
NULL
|
NULL
|
|
43662
|
1592
|
21
|
2026-05-14T13:06:38.505230+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763998505_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43658
|
NULL
|
NULL
|
NULL
|
|
43661
|
1591
|
19
|
2026-05-14T13:06:15.341136+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763975341_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43657
|
NULL
|
NULL
|
NULL
|
|
43660
|
1592
|
20
|
2026-05-14T13:06:08.156287+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763968156_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43658
|
NULL
|
NULL
|
NULL
|
|
43659
|
1591
|
18
|
2026-05-14T13:05:45.031610+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763945031_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43657
|
NULL
|
NULL
|
NULL
|
|
43658
|
1592
|
19
|
2026-05-14T13:05:37.788841+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763937788_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43657
|
1591
|
17
|
2026-05-14T13:05:14.692320+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763914692_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43656
|
1592
|
18
|
2026-05-14T13:05:07.379457+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763907379_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43655
|
NULL
|
NULL
|
NULL
|
|
43655
|
1592
|
17
|
2026-05-14T13:05:01.372318+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763901372_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4284113599095607475
|
-7484559978919001664
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
PhostormProiect vINavicarecodeFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...haCheckAndRetryRemoteMatch.php= custom.log= laravel.logA SF jiminny@localhost]& ho_local Uiminny@localnost& console [PROD]> D OpportunitySyncStratev D Pagination© HubspotPaginationSCo kematchactviyoncrmoojectbetach.ongclientonpA console (EU]in accounts cu)fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.pnp© Activity.php© FixActivitiesOpportunity.php& console SlAGiNG©Paginationcontig.on© Matchermobiect.ongclass rixacciviclesupoorcunlcy excenas commana› _ Prospectsearchstrateo› D Redisv W ServiceTraits• Opportunitysynctra(© SyncCrmEntitiesTrai@ SvncFieldsTrait.phpTWritecrmtrait.php→DUusWeohook© BatchSyncCollector.phfC) BatchSvncRedisService© Client.phpC) ClosedDealStadesServil@ DealFieldsService.phpC) DecorateActivitv nhn© FieldDefinitions.phpC FieldTvoeConverter.on0 HubsnotClientinterfaco© HubspotTokenManager© PayloadBuilder.php© RemoteCrmObjectMani€ ResponseNormalize.ph© Service.php© SyncFieldAction.php© SyncRelatedActivityMa© WebhookSyncBatchPrcv _ IntearationApu> ( Accessors.> M Api• Contio• GoтO.a Filter.• M.Jobs.> D ProspectSearchStrateg> MServiceTraitsC) Dataclient.oho@ DecorateActivitv.nhnm 82^m A12 ^ VC)LocalSearch.nhnl101.@ LocalSearchInterface.pl 10%© RemoteSearch.php@ Service nhnv Mlisteners© ConvertLeadActivities.p 1z2e Duraol ookuneacho nhr• MMiarationclass HubspotPaqnationservicepublic function getPaginatedDataGenerator(Sthis->updateLastRecordId(Spage, $state);// Safely iterate over results with null checkSresulus = spagel"resulcs' ??wrforeach (Sresults as Srow) {Sstate->incrementTotalRecordsO:10€- 101103Sstate->setoffset(Sthis->getNextuffsetSoage)0:- 105.106107Sthis->logPaginationProgress(Sstate, Steamid, $endpoint):} while (Sstate->offset && ! empty(Spage['results']));Sthis->logger->info('[Hubspot] Pagination completed', [itoam 1di => Steamid— 112'total_records_fetched' => $state->totalRecords,itotal plansed secondcl => nound(Sctate->netFlansedSecondcornrecision: ?)D);'average_seconds_per_request' => $state-›requestCount › 0 ? round( num: Sstate->getElapsedSeconds118// Update reference parametersscotal = sstace->coual.SlastRecordId = Sstate->lastRecordId:private function shouldStopPagination(PaginationState Sstate, int SteamId): boolf...}private function handlePaginationStrateav^array Spavloadlarrav SdefaultFilter.PaginationState $state,int SresultsPerPage,int Steamid): array 1...}/ 149private function shouldSwitchToKeysetPagination(PaginationState $state, int SresultsPerPage): bool{...}tunccion processrarclcloantsSteam = Sactivity->getleamorsuser = sactivitv->qetuserossoarticioants=sactivitv->oetparticioantsorforeach (Sparticipants as Sparticipant) 1if (Sparticipant->aetUserId@) |== null |l Sparticinant->qetEmailAddress@) === null) {continue:SemailAddness = Snanticinant->ae+FmailAddnecso•if (SemailHelper->isCompanyEmail(Steam, $emailAddress)) {continue:trySopportunity = $this->find0pportunityInCrm($crmService, SemailAddress, Suser->getId);} catch (Throwable $e) {$this->error( string: 'Could not find opportunity: ' . $e->getMessage());Sopportunity = nullif (Sopportunity === null) {Sthis->resetActivity0pportunitv(Sactivitv):Sactivity->update(['opportunity id' => Sopportunitv->qetldO1):Sthis->info( string: 'Opportunity updated for activity: ' . Sactivity->getidO):private function find0pportunityInCrm(ServiceInterface $crmService, string SemailAddress, int $userId): ?0pportunitprivate function resetActivity0pportunity(Activity Sactivity): void{...}cascadePipedrive SDK Evalua40kal100% S2. Thu 14 May 16:05:01AskJiminnyReportActivityServiceTest vActivity Stage DiscrerImport CRM Activity THubspot Pagination+0 ..I mean manual testing, Whart to test to see it works• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Ask anvthina (84L)÷ @Code SWF-1.6W Windsurf Teams 82:6 (2229 chars, 56 line breaks) UTF-8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43654
|
1592
|
16
|
2026-05-14T13:04:24.297656+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763864297_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43652
|
NULL
|
NULL
|
NULL
|
|
43653
|
1591
|
16
|
2026-05-14T13:04:23.470302+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763863470_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7453110800073781049
|
2498603720148497318
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
43651
|
NULL
|
NULL
|
NULL
|
|
43652
|
1592
|
15
|
2026-05-14T13:03:52.294873+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763832294_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.0787899,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8970380826829891192
|
-9145799838938299278
|
click
|
hybrid
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
PhostormProiect vINavicarecodeFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...haCheckAndRetryRemoteMatch.php= custom.log= laravel.l0gA SF jiminny@localhost]& ho_local Uiminny@localnost& console [PROD]> D OpportunitySyncStraterv D Pagination© HubspotPaginationSCo kematchactviyoncrmoojectbetach.ongclientonpA console (EU]in accounts cu)fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.pnp© Activity.php© FixActivitiesOpportunity.php& console SlAGiNG©Paginationcontig.on© Matchermobiect.ongclass rixacciviclesupoorcunlcy excenas commano› _ Prospectsearchstrateo› D Redisv W ServiceTraits• Opportunitysynctra(© SyncCrmEntitiesTrai@ SvncFieldsTrait.phpTWritecrmtrait.php→DUusWeohook© BatchSyncCollector.phfC) BatchSvncRedisService© Client.phpC) ClosedDealStadesServil@ DealFieldsService.phpC) DecorateActivitv nhn© FieldDefinitions.phpC FieldTvoeConverter.on0 HubsnotClientinterfaco© HubspotTokenManager© PayloadBuilder.php© RemoteCrmObjectMani€ ResponseNormalize.ph© Service.php© SyncFieldAction.php© SyncRelatedActivityMa© WebhookSyncBatchPrcv _ IntearationApu> ( Accessors.> M Api• Contio• GoтO.a Filter.M.lobs> D ProspectSearchStrateg> MServiceTraitsC) Dataclient.oho@ DecorateActivitv.nhnm 82^m A12 ^ VC)LocalSearch.nhnl101.@ LocalSearchInterface.pl 10%© RemoteSearch.php(C) Service nhnv Mlisteners© ConvertLeadActivities.p 1z2e Duraol ookuneacho nhr• MMiarationclass HubspotPaqnationservicepublic function getPaginatedDataGenerator(Sthis->updateLastRecordId(Spage, $state);// Safely iterate over results with null checkSresulus = spagel"resulcs' ??wrforeach (Sresults as Srow) {Sstate->incrementTotalRecordsO:10€- 101103Sstate->setoffset(Sthis->getNextuffsetSoage)0:- 105.106107Sthis->logPaginationProgress(Sstate, Steamid, $endpoint):} while (Sstate->offset && ! empty(Spage['results']));Sthis->logger->info('[Hubspot] Pagination completed', [— 112'total_records_fetched' => $state->totalRecords,itotal plansed secondsl => nound(Sctate->netFlansedSecondcornrecision: ?)D);'average_seconds_per_request' => $state-›requestCount › 0 ? round( num: Sstate->getElapsedSeconds118// Update reference parametersscotal = sstace->coual.SlastRecordId = Sstate->lastRecordId:private function shouldStopPagination(PaginationState $state, int SteamId): boolf...}private function handlePaginationStrateav^array Spavloadlarrav SdefaultFilter.PaginationState $state,int SresultsPerPage,int Steamid): array 1...}/ 149private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool{...}tunccion processrarclcloantsSteam = Sactivity->getleamorsuser = sactivitv->qetuserossoarticioants=sactivitv->oetparticioantsorforeach (Sparticipants as Sparticipant) 1if (Sparticipant->getUserId@) |== null ll Sparticinant->qetEmailAddress() === null) {continue:SemailAddness = Snanticinant->ae+FmailAddnecso•if (SemailHelper->isCompanyEmail(Steam, $emailAddress)) {continue.try fSopportunity = $this->find0pportunityInCrm($crmService, SemailAddress, Suser->getIdO):} catch (Throwable $e) {$this->error( string: 'Could not find opportunity: ' . $e->getMessage());Sopportunity = nullif (Sopportunity === null) {Sthis->resetActivity0pportunitv(Sactivitv):Sactivity->update(['opportunity id' => Sopportunitv->qetldO1):Sthis->info( string: 'Opportunity updated for activity: ' . Sactivity->getidO):private function findOpportunityInCrm(ServiceInterface $crmService, string SemailAddress, int $userId): ?0pportuniprivate function resetActivity0pportunity(Activity Sactivity): void{...}40kal100% S2• Thu 14 May 16:03:52AskJiminnyReportActivityServiceTest vcascadePipedrive SDK EvaluaActivity Stage DiscrerImport CRM Activity Twhere is public tunc+0 ..blic function aetPacinatedDataGeneratorlarray spayloadint &stotal = 0,1: 1GeneratslastRecordid = null/v3/objects/(Stype)/search";Jed weekly usage quota is exhausted. Purchase extra usage to continue usina premium models. Quota resets May 17, 11:00"AN CUTYAsk anvthina (84L)÷ @Code SWF-1.6# 1 file committedJY-20903 revert changes in commandEdit Commit Messaae.W Windsurf Toamc82•6/2220 charc 56 line hreake)UTE.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43651
|
1591
|
15
|
2026-05-14T13:03:52.323670+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763832323_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8970380826829891192
|
-9145799838938299278
|
click
|
hybrid
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
SlackFileEditViewGoHistoryWindowHelpDescribe what you are looking forAneliya AngelovaMessagesAdd canvas@ Files+lhl100% C8• Thu 14 May 16:03:51HomeDMsActivityFilesLaterMoreJiminny ...scnicrat# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Direct messagesP. Aneliya Angelova€. Vasil Vasilev E. Galya Dimitrova&. Stefka Stoyanova: Todor StamatovMario GeorgievNikolay IvanovLo James Graham2 Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...AppsToastJira Cloud6 0Monday, May 11th ~Today ~Aneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук, нали?при останалите CRMi трябва ръчно да се въведатLukas Kovalik 2:47 PMздрастиами не знам по принцип се вика при всичкитрябва да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725Jira CloudX Bug IV-20725 in Jira Cloud[HubSpot] Optimise CRM rematching on delete hubspot ac...StatusReady for QAPriority= MediumAssigneeAA Aneliya AngelovaAs of today at 4:00 PMOpen in Jirait SummariseLukas Kovalik 4:02 PMдазвьни направоMessage Aneliya Angelova...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43650
|
1592
|
14
|
2026-05-14T13:03:50.316417+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763830316_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.0787899,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-392186336938355233
|
-6724486012800957970
|
visual_change
|
accessibility
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error...
|
43649
|
NULL
|
NULL
|
NULL
|
|
43649
|
1592
|
13
|
2026-05-14T13:03:38.183538+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763818183_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.0787899,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7245130343451937535
|
2318460834565305250
|
visual_change
|
accessibility
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43648
|
1591
|
14
|
2026-05-14T13:03:34.850599+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763814850_m1.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Paste
Redo
Cut
Copy
Paste
Paste and Match Style
Se Paste
Redo
Cut
Copy
Paste
Paste and Match Style
Select All
Open DevTools...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Paste","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.065972224,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Redo","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.065972224,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cut","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.065972224,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Copy","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.065972224,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Paste","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.065972224,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Paste and Match Style","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.065972224,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Select All","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.065972224,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Open DevTools","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.065972224,"height":0.024444444},"on_screen":false,"role_description":"text"}]...
|
5413006277286508785
|
-1421033107036992337
|
click
|
hybrid
|
NULL
|
Paste
Redo
Cut
Copy
Paste
Paste and Match Style
Se Paste
Redo
Cut
Copy
Paste
Paste and Match Style
Select All
Open DevTools
SlackFileEditViewGoHistoryWindowHelp→Describe what you are looking forAneliya AngelovaMessagest Add canvas@ Files+lhl100% C8• Thu 14 May 16:03:34HomeDMsActivityFilesLaterMoreJiminny ...scnicrat# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Direct messagesP. Aneliya Angelova€. Vasil Vasilev E. Galya Dimitrova&. Stefka Stoyanova: Todor StamatovMario GeorgievNikolay IvanovLo James Graham2 Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...AppsToastJira Cloud6 0Monday, May 11th ~Today ~Aneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук, нали?при останалите CRMi трябва ръчно да се въведатLukas Kovalik 2:47 PMздрастиами не знам по принцип се вика при всичкитрябва да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725Jira CloudX Bug IV-20725 in Jira Cloud[HubSpot] Optimise CRM rematching on delete hubspot ac...StatusReady for QAPriority= MediumAssigneeAA Aneliya AngelovaAs of today at 4:00 PMOpen in Jirait SummariseLukas Kovalik 4:02 PMдазвьни направоMessage Aneliya Angelova...
|
43646
|
NULL
|
NULL
|
NULL
|
|
43647
|
1592
|
12
|
2026-05-14T13:03:33.727272+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763813727_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.0787899,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"}]...
|
3178589138205942487
|
-8708691559150548542
|
click
|
hybrid
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
PhostormFV faVsco.jsProiect> D OpportunitySyncStratev D Pagination© HubspotPaginationS© PaginationConfig.ph_ Prospectsearchstrateo› D Redisv W ServiceTraits• Opportunitysynctra( SyncCrmEntitiesTrai@ SvncFieldsTrait.phpTWritecrmtrait.php→DUusWeohook© BatchSyncCollector.phfC) BatchSvncRedisService© Client.phpC) ClosedDealStadesServil@ DealFieldsService.phpC) DecorateActivitv nhnC) SieldDefinitions nhnC FieldTvoeConverter.on0 HubsnotClientinterfaco© HubspotTokenManager© PayloadBuilder.php© RemoteCrmObjectMani€ ResponseNormalize.ph© Service.php© SyncFieldAction.php© SyncRelatedActivityMa© WebhookSyncBatchPrcv _ IntearationApu> ( Accessors.> M Api• Contio• GoтO.a Filter.• M.Jobs.> D ProspectSearchStrateg> MServiceTraitsC) Dataclient.oho@ DecorateActivitv.nhnC)LocalSearch.nhnl101.@ LocalSearchInterface.pl 10%© RemoteSearch.php(C) Service nhnv Mlisteners© ConvertLeadActivities.p 1z2e Duraol ookuneacho nhr• MMiarationcodeCheckAndRetryRemoteMatch.phpclientonpcascade40 hl100% S2• Thu 14 May 16:03:33AskJiminnyReportActivityServiceTest vPipedrive SDK EvaluaActivity Stage DiscrerImport CRM Activity T+0 ..© Matchermobiect.ongclass HubspotPaqnat1onServicepublic function getPaginatedDataGenerator(Sthis->updateLastRecordId(Spage, $state);m A12 ^ V// Safely iterate over results with null checksresulus = spagel"results' !! Urforeach (Sresults as Srow) {Sstate->incrementTotalRecordsO:Sstate->setoffset(Sthis->getNextuffsetSoage)0:Sthis->logPaginationProgress(Sstate, Steamid, $endpoint):} while (Sstate->offset && ! empty(Spage['results']));Sthis->logger->info('[Hubspot] Pagination completed', ['total_records_fetched' => $state->totalRecords,itotal plansed cecondsl => nound(Sstate->netFlansedSecondsonrecision: ?)'average_seconds_per_request' => $state-›requestCount › 0 ? round( num: Sstate->getElapsedSeconds118D):// Update reference parametersscocal = sstace->coual.SlastRecordId = Sstate->lastRecordId:• Extract Surround / = :private function shouldStopPagination(PaginationState Sstate, int SteamId): boolf...}private function handlePaginationStrateav^array Spavloadlarrav SdefaultFilter.PaginationState $state,int SresultsPerPage,int Steamid): array 1...}/ 149private function shouldSwitchToKeysetPagination(PaginationState $state, int SresultsPerPage): bool{...}= custom.log= laravel.logA SF jiminny@localhost]& ho_local Uiminny@localnost& console [PROD]A console (EU]in accounts cu)fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.onp© Activity.php© FixActivitiesOpportunity.php& console SlAGiNGclass rixacciviclesupoorcunlcy excenas commana10€- 101103- 105.106107— 112tunccion processrarclcloantsSteam = Sactivity->getleamorsuser = sactivitv->qetuserossoarticioants=sactivitv->oetparticioantsorforeach (Sparticipants as Sparticipant) 1if ($participant->getUserId() |== null || $participant->getEmailAddress() === null) {continue:SemailAddness = Snanticinant->ae+FmailAddnecso•if (SemailHelper->isCompanyEmail(Steam, $emailAddress)) {continue:try fSopportunity = $this->find0pportunityInCrm($crmService, SemailAddress, Suser->getId);} catch (Throwable $e) {$this->error( string: 'Could not find opportunity: ' . $e->getMessage());Sopportunity = nullif (Sopportunity === null) {Sthis->resetActivity0pportunitv(Sactivitv):m 82^Cascade Code x .Kick off a new project. Make changesacross your entre codedaseSactivity->update(['opportunity id' => Sopportunitv->qetldO1):Sthis->info( string: 'Opportunity updated for activity: ' . Sactivity->getidO):private function findOpportunityInCrm(ServiceInterface $crmService, string SemailAddress, int $userId): ?0pportuniprivate function resetActivity0pportunity(Activity Sactivity): void{...}@, Activity Stage Discrepancy Analysis• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00 x# 1 file committedwhere isJY-20903 revert changes in command÷ @Code SWF-1.6Edit Commit Messaae.W Windsurf Toamc82•6 /2220 charc 56 line hreakc) UTF.8io 4 spaces...
|
43644
|
NULL
|
NULL
|
NULL
|
|
43646
|
1591
|
13
|
2026-05-14T13:03:34.273635+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763814273_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"InviteUserToTeamAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkUserAsOnboardableAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncRecordingFlagsAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateTeamMemberAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateUserRolesAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"}]...
|
-7696352825827256269
|
2354488257194734498
|
click
|
accessibility
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43645
|
1591
|
12
|
2026-05-14T13:03:33.758828+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763813758_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
DealInsights, folder
Security, folder
TeamInsights, folder
ActivityActualDate.php...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"InviteUserToTeamAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkUserAsOnboardableAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncRecordingFlagsAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateTeamMemberAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateUserRolesAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EventSubscriber, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinition, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Security, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityActualDate.php","depth":11,"on_screen":false,"role_description":"text"}]...
|
1235908964984695954
|
2354524541078385570
|
click
|
accessibility
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
DealInsights, folder
Security, folder
TeamInsights, folder
ActivityActualDate.php...
|
43643
|
NULL
|
NULL
|
NULL
|
|
43644
|
1592
|
11
|
2026-05-14T13:03:31.528244+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763811528_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.0787899,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-392186336938355233
|
-6724486012800957970
|
click
|
accessibility
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43643
|
1591
|
11
|
2026-05-14T13:03:31.528189+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763811528_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4487525471785219253
|
2354524820251316898
|
click
|
accessibility
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43642
|
1592
|
10
|
2026-05-14T13:03:29.619352+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763809619_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.0787899,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.38231382,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"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.40093085,"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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.73105055,"top":0.123703115,"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\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"InviteUserToTeamAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkUserAsOnboardableAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncRecordingFlagsAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateTeamMemberAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateUserRolesAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"}]...
|
-7696352825827256269
|
2354488257194734498
|
click
|
accessibility
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder...
|
43640
|
NULL
|
NULL
|
NULL
|
|
43641
|
1591
|
10
|
2026-05-14T13:03:29.619364+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763809619_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
DealInsights, folder
Security, folder
TeamInsights, folder
ActivityActualDate.php
ActivityChannel.php
ActivityDurationRange.php
ActivityFilter.php
ActivityPlaylistIn.php
ActivityProviderIn.php
ActivityRecorded.php
ActivityRecordingStopped.php
ActivityScheduledDate.php
ActivityStatusIn.php
ActivityType.php
ActivityUpdatedDate.php
AiCallScoreFilter.php
AutoScoreFilter.php
ClosedDealsFilter.php
CoachingFeedbackAverageScore.php
CoachingFeedbackCoachUserIn.php
CommentCountRange.php
CrmFieldCollection.php
CurrentStage.php
Customer.php
CustomerMonologueDuration.php
CustomerQuestionCount.php
DealAge.php...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","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\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n if ($this->shouldStopPagination($state, $teamId)) {\n break;\n }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n $this->validateTokenIfNeeded($client, $state);\n usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n $state->setTotal($page['total'] ?? 0);\n $this->updateLastRecordId($page, $state);\n\n // Safely iterate over results with null check\n $results = $page['results'] ?? [];\n foreach ($results as $row) {\n $state->incrementTotalRecords();\n yield $row;\n }\n\n $state->setOffset($this->getNextOffset($page));\n $state->incrementRequestCount();\n\n $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"InviteUserToTeamAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkUserAsOnboardableAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncRecordingFlagsAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateTeamMemberAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateUserRolesAction.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EventSubscriber, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinition, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Security, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityActualDate.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityChannel.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityDurationRange.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityFilter.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityPlaylistIn.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityProviderIn.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityRecorded.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityRecordingStopped.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityScheduledDate.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityStatusIn.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityType.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityUpdatedDate.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoreFilter.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutoScoreFilter.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ClosedDealsFilter.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedbackAverageScore.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedbackCoachUserIn.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CommentCountRange.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CrmFieldCollection.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CurrentStage.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Customer.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerMonologueDuration.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerQuestionCount.php","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealAge.php","depth":11,"on_screen":false,"role_description":"text"}]...
|
2019485649024000696
|
2354524541078447010
|
click
|
accessibility
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
InviteUserToTeamAction.php
MarkUserAsOnboardableAction.php
SyncRecordingFlagsAction.php
UpdateTeamMemberAction.php
UpdateUserRolesAction.php
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
DealInsights, folder
Security, folder
TeamInsights, folder
ActivityActualDate.php
ActivityChannel.php
ActivityDurationRange.php
ActivityFilter.php
ActivityPlaylistIn.php
ActivityProviderIn.php
ActivityRecorded.php
ActivityRecordingStopped.php
ActivityScheduledDate.php
ActivityStatusIn.php
ActivityType.php
ActivityUpdatedDate.php
AiCallScoreFilter.php
AutoScoreFilter.php
ClosedDealsFilter.php
CoachingFeedbackAverageScore.php
CoachingFeedbackCoachUserIn.php
CommentCountRange.php
CrmFieldCollection.php
CurrentStage.php
Customer.php
CustomerMonologueDuration.php
CustomerQuestionCount.php
DealAge.php...
|
43639
|
NULL
|
NULL
|
NULL
|
|
43640
|
1592
|
9
|
2026-05-14T13:03:26.621903+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778763806621_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"1 file committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20903 revert changes in command","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"on_screen":true,"value":"JY-20903 revert changes in command","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.0787899,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1307072091985428106
|
-8708621191463335486
|
click
|
hybrid
|
NULL
|
1 file committed
JY-20903 revert changes in comman 1 file committed
JY-20903 revert changes in command
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
PhostormcodeFV faVsco.js#12077 on JY-20903-update_activity-stage-on...hroledey) CheckAndRetryRemoteMatch.php> D OpportunitySyncStraterCo kematchactviyoncrmobjectbetach.ongC) Service.pnpo Cllentonpv D Pagination© HubspotPaginationS©Paginationcontig.on© Matchermobiect.ongm A12 ^ V• _ Prospectsearchstrateo> C Redis› use ...v W ServiceTraits• Opportunitysynctraclass HubspotPaqinationServiceT suncermEntitiestraiTSuncFieldstrait.phpTWritecrmtrait.php→DUusWeohookC BatchSvncCollector.ohrC) BatchSvncRedisServicel© Client.phpC) ClosedDealStadesServi@ DealFieldsService.phpC) DecorateActivitv nhoC) SieldDefinitions nhnC) FieldTvoeConverter.ong0 HubsnotClientinterfaco(e) HubsnotTokenManager(e) DavloadBuilder.ono© RemoteCrmObjectMani€ ResponseNormalize.ph:© Service.php© SyncFieldAction.php© SyncRelatedActivityMac WebhooksyncBatchProv D IntegrationApp> ( Accessors.> M Api• Contio• GoтO.M.Jobs> ProspectSearchStrateg 104> MServiceTraitsC) Dataclient.ohnC) DecorateActivitv. ohnC) Loca|Search.nhn |@ LocalSearchinterface.nl 151© RemoteSearch.phpC) Service nhnv MlistenersC Convortl ondActivitioc re Duraol aokunGacho nhr 140l• MMotadata• MMiarationpublic function -_constructprivate LoggerInterface SLogger* athrows HubsnotExceot.ion*@throws SocialAccountTokeninvalidExcent.ion* Othrows. BadRequest13 usagespublic function getPaginatedDataGeneratorCClient Sclientarray spayload,string suype.int Soffset = 0int &$total = 0?string &$lastRecordId = null): \Generator {.private function shouldStopPagination(PaqinationState $state, int SteamId): boolf...}PaginationState Sstate.int SresultsPerPage.iarray s...private function shouldSwitchToKeysetPagination(PaginationState $state, int SresultsPerPage): boolf...}nnivate function validateTokenTfNeedediClient Sclient PadinationState Sstate)• void ..private function executeSearchRequest(Client Sclient, string $endpoint, array Spayload, PaginationState $sDuched 1 commit to oriain/IV.20902-undate activitv-ctaae-on-onnortunitvcchande /| View null requect (1 minutes aao)= custom.log= laravel.logA SF jiminny@localhost]& ho_local Uiminny@localnost& console [PROD]A console (EU]tiò accounts [EU]fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.pnp© Activity.php© FixActivitiesOpportunity.php& console SlAGiNGclass rixacciviclesupoorcunlcy excenas commanaT106:10811131116tunccion processrarclcloantsSteam = Sactivity->qetTeamosuser = sactivitv->qetuserossoarticioants=sactivitv->oetparticioantsorforeach (Sparticioants as Soarticioant) <if ($participant->getUserId() |== null || $participant->getEmailAddress() === null) {continue:SemailAddness = Snanticinant->ae+FmailAddnecso•if (SemailHelper->isCompanyEmail(Steam, $emailAddress)) {continue.try fSopportunity = $this->find0pportunityInCrm($crmService, SemailAddress, Suser->getId);} catch (Throwable $e) {$this->error( string: 'Could not find opportunity: ' . $e->getMessage());Sopportunity = nullif (Sopportunity === null) {Sthis->resetActivity0pportunitv(Sactivitv):cascadePipedrive SDK EvaluaActivity Stage Discrer40 hl100% S2• Thu 14 May 16:03:26AskJiminnyReportActivityServiceTest vImport CRM Activity T+0 ..m 82^Cascade Code x .Kick off a new project. Make changesacross your entire codedaseSactivity->update(['opportunity id' => Sopportunitv->qetldO1):Sthis->info( string: 'Opportunity updated for activity: ' • Sactivity->getidO):128Tusageprivate function findOpportunityInCrm(ServiceInterface $crmService, string SemailAddress, int $userId): ?Opportuniprivate function resetActivity0pportunity(Activity Sactivity): void{...}@, Activity Stage Discrepancy Analysis• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00 x# 1 file committedwhere isJY-20903 revert changes in command÷ @Code SWF-1.6Edit Commit Messaae.WN Windsurf Toams 26.21UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|