|
10065
|
458
|
13
|
2026-05-08T14:09:34.863843+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249374863_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Proiectrilo May 1/-09.32+0 ..Rate Limit(§)ƠCrmObie Proiectrilo May 1/-09.32+0 ..Rate Limit(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)CascadeA console [STAGING)Hubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Re] local.INF0: [Hubspot] DEBUG Getting headers {w19 .VCallVia executeRequest()?2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,client->fetchMeetingOutcomeTypes():"chunked"client→>tetchcal Activitylypes()getengagementoptions)dboodcsa-SoF"client->fetchOpportunityPipelineStages()getOpportunityOptions()cuent-tecchopportunicyrapetznest)getOpportunityOptions()ecurity":L"max-age=31536000; incLudesubbomains; preload"J,Actions/SyncArchivedProfilesAction.phphcid;desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",CallVia executeRequest ()?client->getOwnersArchived → makeRequestoation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],app/Services/Activity/HubSpot/Service.phpThis service uses Sthis->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(),crmService-scearchCal1cForPeriodO crService-scvncûnnortunitvcrmService_snarceRecords/o.V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.Critical Findings1. Line 1313 — raw searchApi()->doSearch() - matchExactlyByParticipant() in Service.phps#Service.php:1313:0.01,Sresponse = Sthis->cl1ent→>qetNew.nstance()->crm)→>contacts()->searchAp1()→>dosearch(Spay Load)=• Goes completely around the Client wrapper — no 429 handlina, no RateLimitExcention, no rate limit awarenesse"1}}{236535-ec98-4541-b92a-adfa73b69eab",• Uses the search rate limit bucket-905t-4604-9405-0e50551e5545'•Can be replaced with Sthis->client->search('contacts'. Spavload) - same endpoint, same result, adds 429 protection2 line 920-922- comnantes()_ssearchRvhonain() =matchRvDomain()•• Service.php:920-923$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in AxecuteRequest ()3. aetOpportunitvBvldohas executeRequest commented out:D Client.php:238-239Sdeal = Sthis->getNewInstance()->crm()->deals()->basicApi()->getById(• Deliberatelv disabled - this means sinale deal fetches in HubspotSingleSvncStrategy also get no 429 protectionAsk anvthing (84D)fo 4 spaces...
|
NULL
|
9052686617164298025
|
NULL
|
visual_change
|
ocr
|
NULL
|
Proiectrilo May 1/-09.32+0 ..Rate Limit(§)ƠCrmObie Proiectrilo May 1/-09.32+0 ..Rate Limit(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)CascadeA console [STAGING)Hubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Re] local.INF0: [Hubspot] DEBUG Getting headers {w19 .VCallVia executeRequest()?2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,client->fetchMeetingOutcomeTypes():"chunked"client→>tetchcal Activitylypes()getengagementoptions)dboodcsa-SoF"client->fetchOpportunityPipelineStages()getOpportunityOptions()cuent-tecchopportunicyrapetznest)getOpportunityOptions()ecurity":L"max-age=31536000; incLudesubbomains; preload"J,Actions/SyncArchivedProfilesAction.phphcid;desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",CallVia executeRequest ()?client->getOwnersArchived → makeRequestoation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],app/Services/Activity/HubSpot/Service.phpThis service uses Sthis->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(),crmService-scearchCal1cForPeriodO crService-scvncûnnortunitvcrmService_snarceRecords/o.V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.Critical Findings1. Line 1313 — raw searchApi()->doSearch() - matchExactlyByParticipant() in Service.phps#Service.php:1313:0.01,Sresponse = Sthis->cl1ent→>qetNew.nstance()->crm)→>contacts()->searchAp1()→>dosearch(Spay Load)=• Goes completely around the Client wrapper — no 429 handlina, no RateLimitExcention, no rate limit awarenesse"1}}{236535-ec98-4541-b92a-adfa73b69eab",• Uses the search rate limit bucket-905t-4604-9405-0e50551e5545'•Can be replaced with Sthis->client->search('contacts'. Spavload) - same endpoint, same result, adds 429 protection2 line 920-922- comnantes()_ssearchRvhonain() =matchRvDomain()•• Service.php:920-923$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in AxecuteRequest ()3. aetOpportunitvBvldohas executeRequest commented out:D Client.php:238-239Sdeal = Sthis->getNewInstance()->crm()->deals()->basicApi()->getById(• Deliberatelv disabled - this means sinale deal fetches in HubspotSingleSvncStrategy also get no 429 protectionAsk anvthing (84D)fo 4 spaces...
|
10064
|
NULL
|
NULL
|
NULL
|
|
10066
|
458
|
14
|
2026-05-08T14:09:37.981920+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249377981_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Proiect0 (5)CrmObiectsDecorateActivityDummyD Helpe Proiect0 (5)CrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-or:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid;desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWM00.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'rhlo May 1/-09.30AskJiminnyReportActivityServiceTest vCascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Re+0 ..client->getinstance()->dealProperties().allo#NOcliergetobjectFieldso)OpportunitySyncStrategy/CallclientFileVia executeRequest()?Rate LimitjetPaginatedDataGenerator($payload, 'deals')fetchOpportunities()YES, SEARCHclient->getOpportunityById()HubspotSingleSyncStrategy.phpfetchOpportunity()INOSyncRelatedActivityManager.phpCallMethodVia executeRequest()?Rate Limitclient->getPaginatedData(Spayload, 'meeting')YESclient->getMeeting()countMatchingProspects()noteCrmObjectManipulator.phpCallMethodVia executeRequest()?updateCrmActivitvFromLocal()Fields/SpecialFieldDecorator.phpCallVia executeRequest()?Rate Limitclient->fetchDispositionFieldOptions()getEngagementOptions()client->fetchMeetingOutcomeTypes()getEngagementOptions()client->fetchCallActivityTypes() |getEngagementOptions() |client->fetchOpportunityPipelineStages()getOpportunityOptions()client->fetchOpportunityPipelines()getOpportunity0ptions()tNOANOA NOA NOE NO? BURS!BURSTBURSTBURSTBURST|Actions/SyncArchivedProfilesAction.phpCalllMethodVia executeRequest ()?Rate Limitclient->get0wnersArchived → makeRequestoaoo/Services/Activitv/HubSpot/Service.ohoThis service uses Sthis->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(),crCritical FindingsAsk anvthing (84D)C° AdantiveW Windsurf Teamofo 4 spaces...
|
NULL
|
-7269305747531744856
|
NULL
|
visual_change
|
ocr
|
NULL
|
Proiect0 (5)CrmObiectsDecorateActivityDummyD Helpe Proiect0 (5)CrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-or:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid;desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWM00.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'rhlo May 1/-09.30AskJiminnyReportActivityServiceTest vCascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Re+0 ..client->getinstance()->dealProperties().allo#NOcliergetobjectFieldso)OpportunitySyncStrategy/CallclientFileVia executeRequest()?Rate LimitjetPaginatedDataGenerator($payload, 'deals')fetchOpportunities()YES, SEARCHclient->getOpportunityById()HubspotSingleSyncStrategy.phpfetchOpportunity()INOSyncRelatedActivityManager.phpCallMethodVia executeRequest()?Rate Limitclient->getPaginatedData(Spayload, 'meeting')YESclient->getMeeting()countMatchingProspects()noteCrmObjectManipulator.phpCallMethodVia executeRequest()?updateCrmActivitvFromLocal()Fields/SpecialFieldDecorator.phpCallVia executeRequest()?Rate Limitclient->fetchDispositionFieldOptions()getEngagementOptions()client->fetchMeetingOutcomeTypes()getEngagementOptions()client->fetchCallActivityTypes() |getEngagementOptions() |client->fetchOpportunityPipelineStages()getOpportunityOptions()client->fetchOpportunityPipelines()getOpportunity0ptions()tNOANOA NOA NOE NO? BURS!BURSTBURSTBURSTBURST|Actions/SyncArchivedProfilesAction.phpCalllMethodVia executeRequest ()?Rate Limitclient->get0wnersArchived → makeRequestoaoo/Services/Activitv/HubSpot/Service.ohoThis service uses Sthis->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(),crCritical FindingsAsk anvthing (84D)C° AdantiveW Windsurf Teamofo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10067
|
458
|
15
|
2026-05-08T14:09:41.723604+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249381723_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PROD]© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm(->deals(->basicApi©->qetById(3e6) -30ra 0) ->683188-303cAnst0 00)-vom 0= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1] local.INF0: [Hubspot] DEBUG Getting headers {2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid;desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'« console (EU]CascadeHubspot Rate LimitinHubspot Rate Limit ReV19 AVCallclient->getInstance()->companyProperties().all()|Client->get Instance()->contactProperties().all(client->getInstance()->dealProperties().all()client->getInstance()->objectProperties('call/meeting/task').all()jetInstance()->objectProperties($type)->get($id)OpportunitySyncStrategy/CallFileclient->getPaginatedDatagenerator(Spayload, "deals')cllent=>getopportun1tybyld)SyncRelatedActivityManager.phpCallMethodclient->getPaginatedData($payload, 'meeting')fetchRelatedMeetinas()|lient->aetMeetingo)countMatchingProspects()RemoteCrmObjectManipulator.phpCallcliendateEngagement()updateCrmActivityFromLocal()|Fields/SpecialFieldDecorator.phpCallaetEngagementOotions()aetEngagementOotionsoclient->fetchCallActivityTypesogetEngagementOptions()]client->fetchOpportunityPipelineStages()getOpportunity0ptions()client->fetchOpportunityPipelines()getOpportunity0ptions()CallAsk anvthina (&4L)C° AdantiveAskJiminnyReportActivityServiceTest -New CascadeHubspot CRM Call ReMethodVia executeRequest()?getodjectrietas)getObjectFields()getodjectrietas)HNOANOANOgetObjectFields()ANOimportObjectField()INOril o May 1/-09.44+0 ..Rate Limit7 BUKSIVia executeReguest()?Rate Limit• SEARCHBURSTRate LimitVia executeRequest()?Via executeRequest()?ANOVia executeRequest ()?I NOI NOT NO!NOW Windsurf Teamo...
|
NULL
|
198124723861633673
|
NULL
|
visual_change
|
ocr
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PROD]© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm(->deals(->basicApi©->qetById(3e6) -30ra 0) ->683188-303cAnst0 00)-vom 0= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1] local.INF0: [Hubspot] DEBUG Getting headers {2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid;desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'« console (EU]CascadeHubspot Rate LimitinHubspot Rate Limit ReV19 AVCallclient->getInstance()->companyProperties().all()|Client->get Instance()->contactProperties().all(client->getInstance()->dealProperties().all()client->getInstance()->objectProperties('call/meeting/task').all()jetInstance()->objectProperties($type)->get($id)OpportunitySyncStrategy/CallFileclient->getPaginatedDatagenerator(Spayload, "deals')cllent=>getopportun1tybyld)SyncRelatedActivityManager.phpCallMethodclient->getPaginatedData($payload, 'meeting')fetchRelatedMeetinas()|lient->aetMeetingo)countMatchingProspects()RemoteCrmObjectManipulator.phpCallcliendateEngagement()updateCrmActivityFromLocal()|Fields/SpecialFieldDecorator.phpCallaetEngagementOotions()aetEngagementOotionsoclient->fetchCallActivityTypesogetEngagementOptions()]client->fetchOpportunityPipelineStages()getOpportunity0ptions()client->fetchOpportunityPipelines()getOpportunity0ptions()CallAsk anvthina (&4L)C° AdantiveAskJiminnyReportActivityServiceTest -New CascadeHubspot CRM Call ReMethodVia executeRequest()?getodjectrietas)getObjectFields()getodjectrietas)HNOANOANOgetObjectFields()ANOimportObjectField()INOril o May 1/-09.44+0 ..Rate Limit7 BUKSIVia executeReguest()?Rate Limit• SEARCHBURSTRate LimitVia executeRequest()?Via executeRequest()?ANOVia executeRequest ()?I NOI NOT NO!NOW Windsurf Teamo...
|
10066
|
NULL
|
NULL
|
NULL
|
|
10068
|
457
|
13
|
2026-05-08T14:09:48.846410+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249388846_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelp•Lukás Koválik& NotionFileEditViewHistoryWindowHelp•Lukás Koválik's No...ProGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1|TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledge3 Hubspot / Hubspot API callsWork Knowledgetesting keyboard4Todo2 New chat x040lablHubspot API callsLet me do a thorough re-audit, including the Activity HubSpot Service and all remaining directSDK calls.Good - the Activity HubSpot Service uses Sthis->crmService which delegates to the CRMService.php - To direct API calls there. Now let me find the line 1313 ( searchApi()->doSearch()) and all remaining raw SDK calls not going through executeRequest():Now let me search for ALL remaining raw getNewinstance() and getinstance() calls across ALLfiles in the Hubspot directory to make sure nothing is missed:Now let me look at line 918 — the companies()->searchByDomain() call l haven't seen before:Now let me check what getClient() wrapping syncArchivedProfilesAction does and also lookat the SyncArchivedProfilesAction :Good. Now let me also check other Actions classes for any direct API calls:Good - the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg,libphonenumber, Gmail, Office). Now let me check which Client.php methods pass throughexecuteRequest() and which don't:Now I have the full picture. Let me compile the definitive, accurate audit with theexecuteRequest( gateway status clearly marked.Definitive HubSpot API Call Audit100% CFri 8 May 17:09:51Edited just now8 Share ~Suggested actions( Paste* Improve writingProofreadReformatEdit with AlЖ^Е...
|
NULL
|
9013739220236021272
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelp•Lukás Koválik& NotionFileEditViewHistoryWindowHelp•Lukás Koválik's No...ProGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1|TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledge3 Hubspot / Hubspot API callsWork Knowledgetesting keyboard4Todo2 New chat x040lablHubspot API callsLet me do a thorough re-audit, including the Activity HubSpot Service and all remaining directSDK calls.Good - the Activity HubSpot Service uses Sthis->crmService which delegates to the CRMService.php - To direct API calls there. Now let me find the line 1313 ( searchApi()->doSearch()) and all remaining raw SDK calls not going through executeRequest():Now let me search for ALL remaining raw getNewinstance() and getinstance() calls across ALLfiles in the Hubspot directory to make sure nothing is missed:Now let me look at line 918 — the companies()->searchByDomain() call l haven't seen before:Now let me check what getClient() wrapping syncArchivedProfilesAction does and also lookat the SyncArchivedProfilesAction :Good. Now let me also check other Actions classes for any direct API calls:Good - the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg,libphonenumber, Gmail, Office). Now let me check which Client.php methods pass throughexecuteRequest() and which don't:Now I have the full picture. Let me compile the definitive, accurate audit with theexecuteRequest( gateway status clearly marked.Definitive HubSpot API Call Audit100% CFri 8 May 17:09:51Edited just now8 Share ~Suggested actions( Paste* Improve writingProofreadReformatEdit with AlЖ^Е...
|
10063
|
NULL
|
NULL
|
NULL
|
|
10069
|
458
|
16
|
2026-05-08T14:09:50.952499+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249390952_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rilo May 1/-09.0Proiect(§)ƠCrmObiectsDecorateActiv rilo May 1/-09.0Proiect(§)ƠCrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohv(C) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODIA console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {2020 14.21:15 GMl"JoLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -# console leu)CascadeHubspot Rate LimitindHubspot Rate Limit ReNew CascadeHubspot CRM Call Rew19 .Vclient->fetchMeetingOutcomeTypes()client→>fetchCallActivityTypes()client->fetchOpportunityPipelines()getOpportunity0ptions()Actions/SyncArchiveCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → maThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmService->searchcallsForPeriod. crmService->syncOpportunitv. crmService->parseRecords.Critical Findings1. Line 1313-raw cearchAni(_sdoSearch— matchFyactlvRvParticinantinService.ohn.• Service.php:1313$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923— companies->searchBvDomain - matchByDomaino:" Service.oho:920-923ShsAccounts = sthis->[EMAIL]. ScomoanvPropertles• Uses v1 SDK's searchßvDomain which calls a search endnoint — hits the search rate limit buckeAnawve nratontian• Cannot be triviallv replaced with client-ssearch (different endpoint/format). but could be wraoped in executeRequest(03. getOpportunityById() has executeReguest() commented out:#Client.php:238-239Sdeal = Sthis->executeRequest(fn () => Sthis->getNewInstance()->crm()->deals()->basicApi()->qetById(Sdeal = Sthis-saetNeuIinstance@->crmo=>dealso-shasicAnlO=saetBvIdl•Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection,Ask anvthing (84-D)+ « CodeC° Adantiver.al .fo 4 spaces...
|
NULL
|
5525886927799426940
|
NULL
|
visual_change
|
ocr
|
NULL
|
rilo May 1/-09.0Proiect(§)ƠCrmObiectsDecorateActiv rilo May 1/-09.0Proiect(§)ƠCrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohv(C) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODIA console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {2020 14.21:15 GMl"JoLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -# console leu)CascadeHubspot Rate LimitindHubspot Rate Limit ReNew CascadeHubspot CRM Call Rew19 .Vclient->fetchMeetingOutcomeTypes()client→>fetchCallActivityTypes()client->fetchOpportunityPipelines()getOpportunity0ptions()Actions/SyncArchiveCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → maThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmService->searchcallsForPeriod. crmService->syncOpportunitv. crmService->parseRecords.Critical Findings1. Line 1313-raw cearchAni(_sdoSearch— matchFyactlvRvParticinantinService.ohn.• Service.php:1313$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923— companies->searchBvDomain - matchByDomaino:" Service.oho:920-923ShsAccounts = sthis->[EMAIL]. ScomoanvPropertles• Uses v1 SDK's searchßvDomain which calls a search endnoint — hits the search rate limit buckeAnawve nratontian• Cannot be triviallv replaced with client-ssearch (different endpoint/format). but could be wraoped in executeRequest(03. getOpportunityById() has executeReguest() commented out:#Client.php:238-239Sdeal = Sthis->executeRequest(fn () => Sthis->getNewInstance()->crm()->deals()->basicApi()->qetById(Sdeal = Sthis-saetNeuIinstance@->crmo=>dealso-shasicAnlO=saetBvIdl•Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection,Ask anvthing (84-D)+ « CodeC° Adantiver.al .fo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10070
|
457
|
14
|
2026-05-08T14:09:53.756605+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249393756_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewLukás Koválik's No...•n Home NotionFileEditViewLukás Koválik's No...•n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1|TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0HistoryWindowHelpalalProGPHubspot API calls+Work Knowledge3 Hubspot / Hubspot API callsSDK calls.Good - the Activity HubSpot Service uses $this->crmService which delegates to the CRMService.php - no direct API calls there. Now let me find the line 1313 ( searchApi()->doSearch() ) and all remaining raw SDK calls not going through executeRequest():Now let me search for ALL remaining raw getNewinstance() and getinstance() calls across ALLfiles in the Hubspot directory to make sure nothing is missed:Now let me look at line 918 - thecompanies()->searchByDomain() call l haven't seen before:Now let me check what getClient) wrappingsyncArchivedProfilesAction does and also lookat the SyncArchivedProfilesAction:Good. Now let me also check other Actions classes for any direct API calls:Good - the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg,libphonenumber, Gmail, Office). Now let me check which Client.php methods pass throughexecuteRequest() and which don't;Now I have the full picture. Let me compile the definitive, accurate audit with theexecuteRequest( gateway status clearly marked.100% CFri 8 May 17:09:54Edited just now8 Share ~* ...Definitive HubSpot API Call AuditUnderstanding the executeRequest() gatewayexecuteRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95 is the only place that catches 429s and throws RateLimitException. Any call not goingthrough it is silent to the rate limiting system.makeRequest() does NOT call executeRequest() - it bypasses it entirely.Why line 1313 was missed previously...
|
NULL
|
-437986947869261960
|
NULL
|
click
|
ocr
|
NULL
|
NotionFileEditViewLukás Koválik's No...•n Home NotionFileEditViewLukás Koválik's No...•n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1|TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0HistoryWindowHelpalalProGPHubspot API calls+Work Knowledge3 Hubspot / Hubspot API callsSDK calls.Good - the Activity HubSpot Service uses $this->crmService which delegates to the CRMService.php - no direct API calls there. Now let me find the line 1313 ( searchApi()->doSearch() ) and all remaining raw SDK calls not going through executeRequest():Now let me search for ALL remaining raw getNewinstance() and getinstance() calls across ALLfiles in the Hubspot directory to make sure nothing is missed:Now let me look at line 918 - thecompanies()->searchByDomain() call l haven't seen before:Now let me check what getClient) wrappingsyncArchivedProfilesAction does and also lookat the SyncArchivedProfilesAction:Good. Now let me also check other Actions classes for any direct API calls:Good - the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg,libphonenumber, Gmail, Office). Now let me check which Client.php methods pass throughexecuteRequest() and which don't;Now I have the full picture. Let me compile the definitive, accurate audit with theexecuteRequest( gateway status clearly marked.100% CFri 8 May 17:09:54Edited just now8 Share ~* ...Definitive HubSpot API Call AuditUnderstanding the executeRequest() gatewayexecuteRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95 is the only place that catches 429s and throws RateLimitException. Any call not goingthrough it is silent to the rate limiting system.makeRequest() does NOT call executeRequest() - it bypasses it entirely.Why line 1313 was missed previously...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10071
|
458
|
17
|
2026-05-08T14:09:54.482363+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249394482_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rilo May 1/.09.04Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.09.04Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohv(C) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {W19 ^V2020 14.21:15 GMl"JoLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReNew CascadeHubspot CRM Call Reclient->fetchMeetingOutcomeTypes()client→>fetchCallActivityTypes()client->fetchOpportunityPipelines()getOpportunity0ptions()Actions/SyncArchiveCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → maThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmService->searchcallsForPeriod. crmService->syncOpportunitv. crmService->parseRecords.Critical Findings1. Line 1313-raw cearchAni(_sdoSearch— matchFyactlvRvParticinantinService.ohn.• Service.php:1313$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923— companies->searchBvDomain - matchByDomaino:" Service.oho:920-923ShsAccounts = sthis->[EMAIL]. ScomoanvPropertles• Uses v1 SDK's searchßvDomain which calls a search endnoint — hits the search rate limit buckeAnawve nratontian• Cannot be triviallv replaced with client-ssearch (different endpoint/format). but could be wraoped in executeRequest(03. getOpportunityById() has executeReguest() commented out:#Client.php:238-239Sdeal = Sthis->executeRequest(fn () => Sthis->getNewInstance()->crm()->deals()->basicApi()->qetById(Sdeal = Sthis-saetNeuIinstance@->crmo=>dealso-shasicAnlO=saetBvIdl•Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection,Ask anvthing (84-D)+ « CodeC° Adantiver.al .fo 4 spaces...
|
NULL
|
-3844251412157692174
|
NULL
|
click
|
ocr
|
NULL
|
rilo May 1/.09.04Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.09.04Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohv(C) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {W19 ^V2020 14.21:15 GMl"JoLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReNew CascadeHubspot CRM Call Reclient->fetchMeetingOutcomeTypes()client→>fetchCallActivityTypes()client->fetchOpportunityPipelines()getOpportunity0ptions()Actions/SyncArchiveCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → maThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmService->searchcallsForPeriod. crmService->syncOpportunitv. crmService->parseRecords.Critical Findings1. Line 1313-raw cearchAni(_sdoSearch— matchFyactlvRvParticinantinService.ohn.• Service.php:1313$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923— companies->searchBvDomain - matchByDomaino:" Service.oho:920-923ShsAccounts = sthis->[EMAIL]. ScomoanvPropertles• Uses v1 SDK's searchßvDomain which calls a search endnoint — hits the search rate limit buckeAnawve nratontian• Cannot be triviallv replaced with client-ssearch (different endpoint/format). but could be wraoped in executeRequest(03. getOpportunityById() has executeReguest() commented out:#Client.php:238-239Sdeal = Sthis->executeRequest(fn () => Sthis->getNewInstance()->crm()->deals()->basicApi()->qetById(Sdeal = Sthis-saetNeuIinstance@->crmo=>dealso-shasicAnlO=saetBvIdl•Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection,Ask anvthing (84-D)+ « CodeC° Adantiver.al .fo 4 spaces...
|
10069
|
NULL
|
NULL
|
NULL
|
|
10072
|
457
|
15
|
2026-05-08T14:09:56.069285+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249396069_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditView•Lukás Koválik's No...n Home NotionFileEditView•Lukás Koválik's No...n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1|TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0HistoryWindowHelpProGPWork KnowledgeHubspot API calls+3 Hubspot / Hubspot API callsahl100% <47Fri 8 May 17:09:56Edited just nowShare ~Complete Call List (All Files)Legend• V Via executeRequest() - 429s caught, RateLimitException thrown• A NOT via executeRequest() - 429s silently fall through as raw exceptions• • SEARCH rate limit (5 reg/sec)• 4 BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodsearch()getOpportunityByld0batchReadObjects()(deals/companies/contacts)getAccountByld)getContactByld0getContactByEmail0fetchProperty0fetchCallDispositions()fetchOpportunityPipelineStages)fetchOpportunityPipelines() →makeRequest()fetchMeetingOutcomeTypes() /fetchCallActivityTypes() →extractMeetingTypeOptions()makeRequest()(GET/POST/PATCH)Via executeRequest()?YESA NO (commented out)A NOA NOA NOANOA NOA NOA NOA NOA NOA NORate LimitO SEARCH4 BURST4 BURST+ BURST4 BURST+ BURST4 BURST4 BURST4 BURST4 BURST4 BURST4 BURST...
|
NULL
|
-4317452482238723683
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditView•Lukás Koválik's No...n Home NotionFileEditView•Lukás Koválik's No...n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1|TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0HistoryWindowHelpProGPWork KnowledgeHubspot API calls+3 Hubspot / Hubspot API callsahl100% <47Fri 8 May 17:09:56Edited just nowShare ~Complete Call List (All Files)Legend• V Via executeRequest() - 429s caught, RateLimitException thrown• A NOT via executeRequest() - 429s silently fall through as raw exceptions• • SEARCH rate limit (5 reg/sec)• 4 BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodsearch()getOpportunityByld0batchReadObjects()(deals/companies/contacts)getAccountByld)getContactByld0getContactByEmail0fetchProperty0fetchCallDispositions()fetchOpportunityPipelineStages)fetchOpportunityPipelines() →makeRequest()fetchMeetingOutcomeTypes() /fetchCallActivityTypes() →extractMeetingTypeOptions()makeRequest()(GET/POST/PATCH)Via executeRequest()?YESA NO (commented out)A NOA NOA NOANOA NOA NOA NOA NOA NOA NORate LimitO SEARCH4 BURST4 BURST+ BURST4 BURST+ BURST4 BURST4 BURST4 BURST4 BURST4 BURST4 BURST...
|
10070
|
NULL
|
NULL
|
NULL
|
|
10073
|
458
|
18
|
2026-05-08T14:10:02.233631+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249402233_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rilo May 1/.10.04Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.10.04Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohv(C) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {W19 ^V2020 14.21:15 GMl"JoLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReNew CascadeHubspot CRM Call Reclient->fetchMeetingOutcomeTypes()client→>fetchCallActivityTypes()client->fetchOpportunityPipelines()getOpportunity0ptions()Actions/SyncArchiveCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → maThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmService->searchcallsForPeriod. crmService->syncOpportunitv. crmService->parseRecords.Critical Findings1. Line 1313-raw cearchAni(_sdoSearch— matchFyactlvRvParticinantinService.ohn.• Service.php:1313$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923— companies->searchBvDomain - matchByDomaino:" Service.oho:920-923ShsAccounts = sthis->[EMAIL]. ScomoanvPropertles• Uses v1 SDK's searchßvDomain which calls a search endnoint — hits the search rate limit buckeAnawve nratontian• Cannot be triviallv replaced with client-ssearch (different endpoint/format). but could be wraoped in executeRequest(03. getOpportunityById() has executeReguest() commented out:#Client.php:238-239Sdeal = Sthis->executeRequest(fn () => Sthis->getNewInstance()->crm()->deals()->basicApi()->qetById(Sdeal = Sthis-saetNeuIinstance@->crmo=>dealso-shasicAnlO=saetBvIdl•Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection,Ask anvthing (84-D)+ « CodeC° Adantiver.al .fo 4 spaces...
|
NULL
|
7155448665183324741
|
NULL
|
click
|
ocr
|
NULL
|
rilo May 1/.10.04Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.10.04Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohv(C) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {W19 ^V2020 14.21:15 GMl"JoLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReNew CascadeHubspot CRM Call Reclient->fetchMeetingOutcomeTypes()client→>fetchCallActivityTypes()client->fetchOpportunityPipelines()getOpportunity0ptions()Actions/SyncArchiveCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → maThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmService->searchcallsForPeriod. crmService->syncOpportunitv. crmService->parseRecords.Critical Findings1. Line 1313-raw cearchAni(_sdoSearch— matchFyactlvRvParticinantinService.ohn.• Service.php:1313$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923— companies->searchBvDomain - matchByDomaino:" Service.oho:920-923ShsAccounts = sthis->[EMAIL]. ScomoanvPropertles• Uses v1 SDK's searchßvDomain which calls a search endnoint — hits the search rate limit buckeAnawve nratontian• Cannot be triviallv replaced with client-ssearch (different endpoint/format). but could be wraoped in executeRequest(03. getOpportunityById() has executeReguest() commented out:#Client.php:238-239Sdeal = Sthis->executeRequest(fn () => Sthis->getNewInstance()->crm()->deals()->basicApi()->qetById(Sdeal = Sthis-saetNeuIinstance@->crmo=>dealso-shasicAnlO=saetBvIdl•Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection,Ask anvthing (84-D)+ « CodeC° Adantiver.al .fo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10074
|
457
|
16
|
2026-05-08T14:10:03.172896+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249403172_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledge3 Hubspot / Hubspot API callsWork Knowledgetesting keyboard4Todo//2 New chat x0app/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate throu>getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() •Critical Findings1. Line 1313 - rawsearchApi()->doSearch()- matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain ($companyName, $companyProperties);• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:‹ 40alol100% CFri 8 May 17:10:05Edited just now8 Share ~* ...Q Search actions...AgDefaultAgSerifAgMonoCopy linkCopy page contentsDuplicateMove to*DЖАРMove to TrashPresent Beta*TPAvailable offlineAl Small text*→ Full widtaCustomize page• Lock page{ Use with AlSuggest editsAX Translate→ Undo*Z* ImportF ExportTurn into wikiUpdates & analytics$deal = $this->executeRequest(fn () = $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->getNewInstance()->crm()→>deals()->basicApi()->getById(...
|
NULL
|
3748005707411868595
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledge3 Hubspot / Hubspot API callsWork Knowledgetesting keyboard4Todo//2 New chat x0app/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate throu>getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() •Critical Findings1. Line 1313 - rawsearchApi()->doSearch()- matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain ($companyName, $companyProperties);• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:‹ 40alol100% CFri 8 May 17:10:05Edited just now8 Share ~* ...Q Search actions...AgDefaultAgSerifAgMonoCopy linkCopy page contentsDuplicateMove to*DЖАРMove to TrashPresent Beta*TPAvailable offlineAl Small text*→ Full widtaCustomize page• Lock page{ Use with AlSuggest editsAX Translate→ Undo*Z* ImportF ExportTurn into wikiUpdates & analytics$deal = $this->executeRequest(fn () = $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->getNewInstance()->crm()→>deals()->basicApi()->getById(...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10075
|
457
|
17
|
2026-05-08T14:10:07.318192+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249407318_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledge3 Hubspot / Hubspot API callsWork Knowledgetesting keyboard4Todo//2 New chat x0app/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate throu>getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() •Critical Findings1. Line 1313 - rawsearchApi()->doSearch()- matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain ($companyName, $companyProperties);• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:‹ 40labl100% CFri 8 May 17:10:07Edited just now8 Share ~* ...Search actions...AgDefaultAgSerifAgMonoCopy linkCopy page contentsDuplicateMove to*DЖАРMove to TrashPresentBeta*TPAvailable offlineAl Small text+→ Fwj widthCustomize page• Lock page{ Use with AlSuggest editsAX Translate→ Undo*Z* ImportF ExportTurn into wikiUpdates & analytics$deal = $this->executeRequest(fn () = $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->getNewInstance()->crm()→>deals()->basicApi()->getById(...
|
NULL
|
-1814129944730592029
|
NULL
|
click
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledge3 Hubspot / Hubspot API callsWork Knowledgetesting keyboard4Todo//2 New chat x0app/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate throu>getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() •Critical Findings1. Line 1313 - rawsearchApi()->doSearch()- matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain ($companyName, $companyProperties);• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:‹ 40labl100% CFri 8 May 17:10:07Edited just now8 Share ~* ...Search actions...AgDefaultAgSerifAgMonoCopy linkCopy page contentsDuplicateMove to*DЖАРMove to TrashPresentBeta*TPAvailable offlineAl Small text+→ Fwj widthCustomize page• Lock page{ Use with AlSuggest editsAX Translate→ Undo*Z* ImportF ExportTurn into wikiUpdates & analytics$deal = $this->executeRequest(fn () = $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->getNewInstance()->crm()→>deals()->basicApi()->getById(...
|
10074
|
NULL
|
NULL
|
NULL
|
|
10076
|
457
|
18
|
2026-05-08T14:10:28.823010+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249428823_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelp•Lukás Koválik& NotionFileEditViewHistoryWindowHelp•Lukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1ĐTododevEvaluationKnowledgeca IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4 Todo& New chat *03 Hubspot / Hubspot API callsServiceTraits/SyncCrmEntitiesTrait.phpCallMethodclient->getAssociationsData()getAssociationDataForCollectioclient->getPaginatedDataGenerator($payload, 'contacts")syncContacts()client->getContactByld0syncSingleContact()client->getPaginatedDataGenerator($payload, 'companies')syncAccounts()client->getAccountByld)syncSingleAccount()client->getContactsBylds0batchFetchContacts()client->getCompaniesBylds() (x2)batchFetchCompanies/ForAssociations()ServiceTraits/OpportunitySyncTrait.phpCallMethodclient->getOpportunitiesBylds()syncOpportunitiesBatch()client->getAssociationsData('deals,'companies')syncOpportunitiesBatch()client-tacts')syncOpportunitiesBatch()>getAssociationsData('deals','conclient->getCompaniesBylds() orgetContactsBylds()syncCrmObjects()lanl100% <7*Edited just nowFri 8 May 17:10:308 Share ~Via executeRequest()?A NOV YESA NOV YESA NOA NOA NORate Limit4 BURST• SEARCH4 BURST• SEARCH4 BURST4 BURST4 BURSTVia executeRequest()?A NOA NOA NOA NORate Limit4 BURST4 BURST4 BURST4 BURST...
|
NULL
|
2097688637185984538
|
NULL
|
click
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelp•Lukás Koválik& NotionFileEditViewHistoryWindowHelp•Lukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1ĐTododevEvaluationKnowledgeca IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4 Todo& New chat *03 Hubspot / Hubspot API callsServiceTraits/SyncCrmEntitiesTrait.phpCallMethodclient->getAssociationsData()getAssociationDataForCollectioclient->getPaginatedDataGenerator($payload, 'contacts")syncContacts()client->getContactByld0syncSingleContact()client->getPaginatedDataGenerator($payload, 'companies')syncAccounts()client->getAccountByld)syncSingleAccount()client->getContactsBylds0batchFetchContacts()client->getCompaniesBylds() (x2)batchFetchCompanies/ForAssociations()ServiceTraits/OpportunitySyncTrait.phpCallMethodclient->getOpportunitiesBylds()syncOpportunitiesBatch()client->getAssociationsData('deals,'companies')syncOpportunitiesBatch()client-tacts')syncOpportunitiesBatch()>getAssociationsData('deals','conclient->getCompaniesBylds() orgetContactsBylds()syncCrmObjects()lanl100% <7*Edited just nowFri 8 May 17:10:308 Share ~Via executeRequest()?A NOV YESA NOV YESA NOA NOA NORate Limit4 BURST• SEARCH4 BURST• SEARCH4 BURST4 BURST4 BURSTVia executeRequest()?A NOA NOA NOA NORate Limit4 BURST4 BURST4 BURST4 BURST...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10077
|
458
|
19
|
2026-05-08T14:10:28.924531+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249428924_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rilo May 1/.10.34Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.10.34Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohv(C) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {W19 ^V2020 14.21:15 GMl"JoLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReNew CascadeHubspot CRM Call Reclient->fetchMeetingOutcomeTypes()client→>fetchCallActivityTypes()client->fetchOpportunityPipelines()getOpportunity0ptions()Actions/SyncArchiveCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → maThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmService->searchcallsForPeriod. crmService->syncOpportunitv. crmService->parseRecords.Critical Findings1. Line 1313-raw cearchAni(_sdoSearch— matchFyactlvRvParticinantinService.ohn.• Service.php:1313$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923— companies->searchBvDomain - matchByDomaino:" Service.oho:920-923ShsAccounts = sthis->[EMAIL]. ScomoanvPropertles• Uses v1 SDK's searchßvDomain which calls a search endnoint — hits the search rate limit buckeAnawve nratontian• Cannot be triviallv replaced with client-ssearch (different endpoint/format). but could be wraoped in executeRequest(03. getOpportunityById() has executeReguest() commented out:#Client.php:238-239Sdeal = Sthis->executeRequest(fn () => Sthis->getNewInstance()->crm()->deals()->basicApi()->qetById(Sdeal = Sthis-saetNeuIinstance@->crmo=>dealso-shasicAnlO=saetBvIdl•Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection,Ask anvthing (84-D)+ « CodeC° Adantiver.al .fo 4 spaces...
|
NULL
|
3106496118143009307
|
NULL
|
click
|
ocr
|
NULL
|
rilo May 1/.10.34Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.10.34Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c)Batchsynccollector.phpc) BatchSyncRedisServiceC) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohv(C) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator:".', Stlelds).} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'.'reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI# console leu)A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {W19 ^V2020 14.21:15 GMl"JoLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReNew CascadeHubspot CRM Call Reclient->fetchMeetingOutcomeTypes()client→>fetchCallActivityTypes()client->fetchOpportunityPipelines()getOpportunity0ptions()Actions/SyncArchiveCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → maThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmService->searchcallsForPeriod. crmService->syncOpportunitv. crmService->parseRecords.Critical Findings1. Line 1313-raw cearchAni(_sdoSearch— matchFyactlvRvParticinantinService.ohn.• Service.php:1313$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923— companies->searchBvDomain - matchByDomaino:" Service.oho:920-923ShsAccounts = sthis->[EMAIL]. ScomoanvPropertles• Uses v1 SDK's searchßvDomain which calls a search endnoint — hits the search rate limit buckeAnawve nratontian• Cannot be triviallv replaced with client-ssearch (different endpoint/format). but could be wraoped in executeRequest(03. getOpportunityById() has executeReguest() commented out:#Client.php:238-239Sdeal = Sthis->executeRequest(fn () => Sthis->getNewInstance()->crm()->deals()->basicApi()->qetById(Sdeal = Sthis-saetNeuIinstance@->crmo=>dealso-shasicAnlO=saetBvIdl•Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection,Ask anvthing (84-D)+ « CodeC° Adantiver.al .fo 4 spaces...
|
10073
|
NULL
|
NULL
|
NULL
|
|
10078
|
457
|
19
|
2026-05-08T14:10:33.305943+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249433305_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...ProGPWork KnowledgeHubspot API calls+3 Hubspot / Hubspot API callsalolA100% CFri 8 May 17:10:34n Home1000TestDailyAgents+ New agentWorkspace& Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1ĐTododevEvaluationKnowledgera IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0Edited just now8 Share ~...Hubspot API callsLet me do a thorough re-audit, including the Activity HubSpot Service and all remaining direct SDK calls.Good - the Activity HubSpot Service uses $this->frmService which delegates to the CRM Service.php - no direct API calls there. Now let me find theline 1313 ( searchApi()->doSearch() ) and all remaining raw SDK calls not going through executeRequest():Now let me search for ALL remaining raw getNewinstance() and getinstance() calls across ALL files in the Hubspot directory to make sure nothing ismissed:Now let me look at line 918 — the companies()->searchByDomain() call l haven't seen before:Now let me check what getClient() wrapping syncArchivedProfilesAction does and also look at the SyncArchivedProfilesAction :Good. Now let me also check other Actions classes for any direct API calls:Good - the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg, libphonenumber, Gmail, Office). Now let me check whichClient.php methods pass through executeRequest() and which don't:Now I have the full picture. Let me compile the definitive, accurate audit with the executeRequest() gateway status clearly marked.Definitive HubSpot API Call AuditUnderstanding the executeRequest( gatewayexecuteRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95 is the only place that catches 429s and throwsRateLimitException . Any call not going through it is silent to the rate limiting system....
|
NULL
|
6403883780158718854
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...ProGPWork KnowledgeHubspot API calls+3 Hubspot / Hubspot API callsalolA100% CFri 8 May 17:10:34n Home1000TestDailyAgents+ New agentWorkspace& Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1ĐTododevEvaluationKnowledgera IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0Edited just now8 Share ~...Hubspot API callsLet me do a thorough re-audit, including the Activity HubSpot Service and all remaining direct SDK calls.Good - the Activity HubSpot Service uses $this->frmService which delegates to the CRM Service.php - no direct API calls there. Now let me find theline 1313 ( searchApi()->doSearch() ) and all remaining raw SDK calls not going through executeRequest():Now let me search for ALL remaining raw getNewinstance() and getinstance() calls across ALL files in the Hubspot directory to make sure nothing ismissed:Now let me look at line 918 — the companies()->searchByDomain() call l haven't seen before:Now let me check what getClient() wrapping syncArchivedProfilesAction does and also look at the SyncArchivedProfilesAction :Good. Now let me also check other Actions classes for any direct API calls:Good - the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg, libphonenumber, Gmail, Office). Now let me check whichClient.php methods pass through executeRequest() and which don't:Now I have the full picture. Let me compile the definitive, accurate audit with the executeRequest() gateway status clearly marked.Definitive HubSpot API Call AuditUnderstanding the executeRequest( gatewayexecuteRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95 is the only place that catches 429s and throwsRateLimitException . Any call not going through it is silent to the rate limiting system....
|
10076
|
NULL
|
NULL
|
NULL
|
|
10079
|
457
|
20
|
2026-05-08T14:10:38.632470+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249438632_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelp•Lukás Koválik& NotionFileEditViewHistoryWindowHelp•Lukás Koválik's No...ProGPHubspot API calls+n HomeWork Knowledge3 Hubspot / Hubspot API calls1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprint100% 0 8Fri 8 May 17:10:41Edited just now8 Share ~* ...Let me do a thorough re-audit, including the Activity HubSpot Service and all remaining direct SDK calls.Good - the Activity HubSpot Service uses $this->crmService which delegates to the CRM Service.php - no direct API calls there. Now let me find theline 1313 ( searchApi()->doSearch() ) and all remaining raw SDK calls not going through executeRequest():Now let me search for ALL remaining raw getNewinstance() and getinstance() calls across ALL files in the Hubspot directory to make sure nothing ismissed:Now let me look at line 918 - thecompanies()->searchByDomain()call i haven't seen before:Now let me check what getClient() wrappingsyncArchivedProfilesAction does and also look at the SyncArchivedProfilesAction:Good. Now let me also check other Actions classes for any direct API calls:Good — the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg, libphonenumber, Gmail, Office). Now let me check whichClient.php methods pass through executeRequest() and which don't:Now I have the full picture. Let me compile the definitive, accurate audit with the executeRequest() gateway status clearly marked.ĐdevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4 Todo2 New chat x0Definitive HubSpot API Call AuditUnderstanding the executeRequest() gatewayexecuteRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95 is the only place that catches 429s and throwsRateLimitException . Any call not going through it is silent to the rate limiting system.makeRequest() does NOT call executeRequest() - it bypasses it entirely.Why line 1313 was missed previouslySresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch ($payload);...
|
NULL
|
8383567329175075045
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelp•Lukás Koválik& NotionFileEditViewHistoryWindowHelp•Lukás Koválik's No...ProGPHubspot API calls+n HomeWork Knowledge3 Hubspot / Hubspot API calls1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprint100% 0 8Fri 8 May 17:10:41Edited just now8 Share ~* ...Let me do a thorough re-audit, including the Activity HubSpot Service and all remaining direct SDK calls.Good - the Activity HubSpot Service uses $this->crmService which delegates to the CRM Service.php - no direct API calls there. Now let me find theline 1313 ( searchApi()->doSearch() ) and all remaining raw SDK calls not going through executeRequest():Now let me search for ALL remaining raw getNewinstance() and getinstance() calls across ALL files in the Hubspot directory to make sure nothing ismissed:Now let me look at line 918 - thecompanies()->searchByDomain()call i haven't seen before:Now let me check what getClient() wrappingsyncArchivedProfilesAction does and also look at the SyncArchivedProfilesAction:Good. Now let me also check other Actions classes for any direct API calls:Good — the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg, libphonenumber, Gmail, Office). Now let me check whichClient.php methods pass through executeRequest() and which don't:Now I have the full picture. Let me compile the definitive, accurate audit with the executeRequest() gateway status clearly marked.ĐdevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4 Todo2 New chat x0Definitive HubSpot API Call AuditUnderstanding the executeRequest() gatewayexecuteRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95 is the only place that catches 429s and throwsRateLimitException . Any call not going through it is silent to the rate limiting system.makeRequest() does NOT call executeRequest() - it bypasses it entirely.Why line 1313 was missed previouslySresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch ($payload);...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10080
|
458
|
20
|
2026-05-08T14:10:40.490614+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249440490_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Proiectril o May 1/:10.4+0 ..0 (5)CrmObiectsDecora Proiectril o May 1/:10.4+0 ..0 (5)CrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i (1 Sdeal instancenf NealWithAccociations) «thnow new CrmFycentiond messaae"Neal not found!)•notunn: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19AV2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Relimiting system,Why line 1313 was missed previously=) Service.php:1313Sresponse = sth1s->cl1ent→>getNewinstance()→>crm()→>contacts()→>searchAp1()→>dosearch(Spay load)=The previous audit searched for sthis->client→> method calls on the Client wrapper class. This line goes directly to the HubSpot SDK via getNewInstance()which returns a raw Discovery object - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files) |Legend! NOT via executeRequest () - 429s silently fall through as raw exceptions* SEARCH rate limit (5 req/sec)BURST rate limit (100-200 req/10s)Client.php - All API MethodsaetOpportunityBvIdo)fetchCallDispositionso)tetchopportun1tyPipel1neStagesfetchOpportunityPipelines() → makeRequest()fetchMeetingOutcomeTypes() / fetchCallActivityTypes() → extractMeetingType0ptions()eRequest() (GETPOST/PATCH)|createMeeting() → makeRequest() |lateMeeting() → makeRequest() |deleteFnaagement ()Ask anvthing (84.D)E AdaptivoVia executeRequest()?Rate LimitVYES4 SEARCHNO (cented out) |÷ BURSTI!NO!NOA NOA NOA NONO- BURST÷ BURST9 BURST- BURST, BURSTW Windsurf Teams 238:34 UTF-8 4 spaces...
|
NULL
|
867063571777703381
|
NULL
|
visual_change
|
ocr
|
NULL
|
Proiectril o May 1/:10.4+0 ..0 (5)CrmObiectsDecora Proiectril o May 1/:10.4+0 ..0 (5)CrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i (1 Sdeal instancenf NealWithAccociations) «thnow new CrmFycentiond messaae"Neal not found!)•notunn: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19AV2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Relimiting system,Why line 1313 was missed previously=) Service.php:1313Sresponse = sth1s->cl1ent→>getNewinstance()→>crm()→>contacts()→>searchAp1()→>dosearch(Spay load)=The previous audit searched for sthis->client→> method calls on the Client wrapper class. This line goes directly to the HubSpot SDK via getNewInstance()which returns a raw Discovery object - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files) |Legend! NOT via executeRequest () - 429s silently fall through as raw exceptions* SEARCH rate limit (5 req/sec)BURST rate limit (100-200 req/10s)Client.php - All API MethodsaetOpportunityBvIdo)fetchCallDispositionso)tetchopportun1tyPipel1neStagesfetchOpportunityPipelines() → makeRequest()fetchMeetingOutcomeTypes() / fetchCallActivityTypes() → extractMeetingType0ptions()eRequest() (GETPOST/PATCH)|createMeeting() → makeRequest() |lateMeeting() → makeRequest() |deleteFnaagement ()Ask anvthing (84.D)E AdaptivoVia executeRequest()?Rate LimitVYES4 SEARCHNO (cented out) |÷ BURSTI!NO!NOA NOA NOA NONO- BURST÷ BURST9 BURST- BURST, BURSTW Windsurf Teams 238:34 UTF-8 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10081
|
457
|
21
|
2026-05-08T14:10:44.980098+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249444980_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpablA100% <7* NotionFileEditViewHistoryWindowHelpablA100% <7*•Lukás Koválik's No...ProGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintĐdevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4TodoWork Knowledge / ../ Hubspot / Hubspot API callsClient.php methods pass through executeRequest() and which don't:Edited just nowNow I have the full picture. Let me compile the definitive, accurate audit with the executeRequest() gateway status clearly marked.Definitive HubSpot API Call AuditUnderstanding the executeRequest() gateway+ :executeRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95 is the only place that catches 429s and throwsRateLimitException . Any call not going threugh it is silent to the rate limiting system.makeRequest() does NOT call executeRequest() - it bypasses it entirely.Why line 1313 was missed previouslySresponse = $this->client→>getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);The previous audit searched forSthis->client->method calls on the Client wrapper class. This line goes directly to the HubSpot SDK viagetNewinstance() which returns a raw Discoveryobject - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files)Legend• V Via executeRequest) - 429s caught, RateLimitException thrown• A NOT via executeRequest() - 429s silently fall through as raw exceptions• • SEARCH rate limit (5 req/sec)• 4 BURST rate limit (100-200 req/10s)2 New chat x0Fri 8 May 17:10:478 Share ~......
|
NULL
|
1964438774000148781
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpablA100% <7* NotionFileEditViewHistoryWindowHelpablA100% <7*•Lukás Koválik's No...ProGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintĐdevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4TodoWork Knowledge / ../ Hubspot / Hubspot API callsClient.php methods pass through executeRequest() and which don't:Edited just nowNow I have the full picture. Let me compile the definitive, accurate audit with the executeRequest() gateway status clearly marked.Definitive HubSpot API Call AuditUnderstanding the executeRequest() gateway+ :executeRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95 is the only place that catches 429s and throwsRateLimitException . Any call not going threugh it is silent to the rate limiting system.makeRequest() does NOT call executeRequest() - it bypasses it entirely.Why line 1313 was missed previouslySresponse = $this->client→>getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);The previous audit searched forSthis->client->method calls on the Client wrapper class. This line goes directly to the HubSpot SDK viagetNewinstance() which returns a raw Discoveryobject - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files)Legend• V Via executeRequest) - 429s caught, RateLimitException thrown• A NOT via executeRequest() - 429s silently fall through as raw exceptions• • SEARCH rate limit (5 req/sec)• 4 BURST rate limit (100-200 req/10s)2 New chat x0Fri 8 May 17:10:478 Share ~......
|
10079
|
NULL
|
NULL
|
NULL
|
|
10082
|
458
|
21
|
2026-05-08T14:10:46.380692+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249446380_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):throw Se.i (1 Sdeal instancenf NealWithAccociations) «thnow new CrmFycentiond messaae"Neal not found!)•notunn] local.INF0: [Hubspot] DEBUG Getting headers {w19AV2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Relimiting systemWhy line 1313 was missed previously=) Service.php:1313Sresponse = sth1s->cl1ent→>getNewinstance()→>crm()→>contacts()→>searchAp1()→>dosearch(Spay load)=The previous audit searched for sthis->client→> method calls on the Client wrapper class. This line goes directly to the HubSpot SDK via getNewInstance()which returns a raw Discovery object - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files) |Legend! NOT via executeRequest () - 429s silently fall through as raw exceptions* SEARCH rate limit (5 req/sec)BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodaetOpportunityBvIdo)ril o May 1/:10.4/+0 ..fetchCallDispositionso)tetchopportun1tyPipel1neStagesfetchOpportunityPipelines() → makeRequest()fetchMeetingOutcomeTypes() / fetchCallActivityTypes() → extractMeetingType0ptions()makeRequest() (GET/POST/PATCH)|createMeeting() → makeRequest() |lateMeeting() → makeRequest() |deleteFnaagement ()Ask anvthing (84D)C° AdantiveVia executeRequest()?Rate LimitVYES4 SEARCHNO (cented out) |÷ BURSTI!NO!NOA NOA NOA NONO- BURST÷ BURST9 BURST- BURST, BURSTW Windsurf Teams 238:34 UTF-8 4 spaces...
|
NULL
|
1809476560743574387
|
NULL
|
click
|
ocr
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):throw Se.i (1 Sdeal instancenf NealWithAccociations) «thnow new CrmFycentiond messaae"Neal not found!)•notunn] local.INF0: [Hubspot] DEBUG Getting headers {w19AV2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Relimiting systemWhy line 1313 was missed previously=) Service.php:1313Sresponse = sth1s->cl1ent→>getNewinstance()→>crm()→>contacts()→>searchAp1()→>dosearch(Spay load)=The previous audit searched for sthis->client→> method calls on the Client wrapper class. This line goes directly to the HubSpot SDK via getNewInstance()which returns a raw Discovery object - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files) |Legend! NOT via executeRequest () - 429s silently fall through as raw exceptions* SEARCH rate limit (5 req/sec)BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodaetOpportunityBvIdo)ril o May 1/:10.4/+0 ..fetchCallDispositionso)tetchopportun1tyPipel1neStagesfetchOpportunityPipelines() → makeRequest()fetchMeetingOutcomeTypes() / fetchCallActivityTypes() → extractMeetingType0ptions()makeRequest() (GET/POST/PATCH)|createMeeting() → makeRequest() |lateMeeting() → makeRequest() |deleteFnaagement ()Ask anvthing (84D)C° AdantiveVia executeRequest()?Rate LimitVYES4 SEARCHNO (cented out) |÷ BURSTI!NO!NOA NOA NOA NONO- BURST÷ BURST9 BURST- BURST, BURSTW Windsurf Teams 238:34 UTF-8 4 spaces...
|
10080
|
NULL
|
NULL
|
NULL
|
|
10083
|
457
|
22
|
2026-05-08T14:10:49.311188+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249449311_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpablA100% <Lu NotionFileEditViewHistoryWindowHelpablA100% <Lukás Koválik's No...PrcGPHubspot API callsn Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1|ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0Work KnowledgeleoeeHubspot / Hubspot API callsEdited just nowNow let me look at line 918 - thecompanies()->searchByDomain()call I haven't seen before:Now let me check what getClient() wrapping syncArchivedProfilesAction does and also look at the SyncArchivedProfilesAction :Good. Now let me also check other Actions classes for any direct API calls:Good - the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg, libphonenumber, Gmail, Office). Now let me check whichClient.php methods pass through executeRequest() and which don't:Now I have the full picture. Let me compile the definitive, accurate audit with the executeRequest() gateway status clearly marked.Definitive HubSpot API Call AuditUnderstanding the executeRequest() gatewayexecuteRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95is the only place that catches 429s and throwsRateLimitException . Any call not going through it is silent to the rate limiting system.makeRequest() does NOT call executeRequest() - it bypasses it entirely.Why line 1313 was missed previouslySresponse = $this->client→>getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);The previous audit searched forSthis->client->getNewinstance() which returns a raw Discoverymethod calls on the Client wrapper class. This line goes directly to the HubSpot SDK viaobject — it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files)LegendFri 8 May 17:10:49à Share v* ......
|
NULL
|
-8625942921475171151
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpablA100% <Lu NotionFileEditViewHistoryWindowHelpablA100% <Lukás Koválik's No...PrcGPHubspot API callsn Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1|ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0Work KnowledgeleoeeHubspot / Hubspot API callsEdited just nowNow let me look at line 918 - thecompanies()->searchByDomain()call I haven't seen before:Now let me check what getClient() wrapping syncArchivedProfilesAction does and also look at the SyncArchivedProfilesAction :Good. Now let me also check other Actions classes for any direct API calls:Good - the getinstance() hits outside the Hubspot CRM directory are all unrelated (ffmpeg, libphonenumber, Gmail, Office). Now let me check whichClient.php methods pass through executeRequest() and which don't:Now I have the full picture. Let me compile the definitive, accurate audit with the executeRequest() gateway status clearly marked.Definitive HubSpot API Call AuditUnderstanding the executeRequest() gatewayexecuteRequest() at @/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Client.php:75-95is the only place that catches 429s and throwsRateLimitException . Any call not going through it is silent to the rate limiting system.makeRequest() does NOT call executeRequest() - it bypasses it entirely.Why line 1313 was missed previouslySresponse = $this->client→>getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);The previous audit searched forSthis->client->getNewinstance() which returns a raw Discoverymethod calls on the Client wrapper class. This line goes directly to the HubSpot SDK viaobject — it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files)LegendFri 8 May 17:10:49à Share v* ......
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10084
|
457
|
23
|
2026-05-08T14:10:53.139228+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249453139_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditView•••Lukás Koválik's No...n Ho NotionFileEditView•••Lukás Koválik's No...n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- Work• Jira ticketE View of SprintDailyPlanSprint|Stefka 1-1|ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0HistoryWindowHelp‹ 50lbl100% <47*Fri 8 May 17:10:55Hubspot API callsPress 'space' for Al or 'l' for commandsComplete Call List (All Files)Legend• • Via executeRequest() - 429s caught, RateLimitException thrown• A NOT via executeRequest() - 429s silently fall through as raw exceptions• • SEARCH rate limit (5 req/sec)• 4 BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodsearch()getOpportunityByld)batchReadObjects()(deals/companies/contacts)getAccountByld()getContactByld()getContactByEmailOToten DranortulalVia executeRequest()?V YESA NO (commented out)A NOA NOA NOA NOA NGIRate Limit• SEARCH4 BURST4 BURST4 BURST4 BURST4 BURST& pIIpST...
|
NULL
|
366887414207463560
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditView•••Lukás Koválik's No...n Ho NotionFileEditView•••Lukás Koválik's No...n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- Work• Jira ticketE View of SprintDailyPlanSprint|Stefka 1-1|ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0HistoryWindowHelp‹ 50lbl100% <47*Fri 8 May 17:10:55Hubspot API callsPress 'space' for Al or 'l' for commandsComplete Call List (All Files)Legend• • Via executeRequest() - 429s caught, RateLimitException thrown• A NOT via executeRequest() - 429s silently fall through as raw exceptions• • SEARCH rate limit (5 req/sec)• 4 BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodsearch()getOpportunityByld)batchReadObjects()(deals/companies/contacts)getAccountByld()getContactByld()getContactByEmailOToten DranortulalVia executeRequest()?V YESA NO (commented out)A NOA NOA NOA NOA NGIRate Limit• SEARCH4 BURST4 BURST4 BURST4 BURST4 BURST& pIIpST...
|
10083
|
NULL
|
NULL
|
NULL
|
|
10085
|
457
|
24
|
2026-05-08T14:11:06.940137+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249466940_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelp‹ 40lth)Lukás K NotionFileEditViewHistoryWindowHelp‹ 40lth)Lukás Koválik's No...ProGF+n Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgexe IdeasPrivateE Home viewsHubspot API callsWork KnowledgeIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x03 Hubspot / Hubspot API callsgetMeeting0)getAssociationsData()A NOANO4 BURST4 BURSTaddAssociations() →makeRequest()A NO4 BURSTremoveAssociations() →makeRequest()A NO4 BURSTKey finding: Only search 0 passes through execute Re quest). Every other Client,php method bypasses it.Service.php - All Call SitesLineCallMethod context~299getCrmField()Via executeRequest()?A NOgetinstance)->companyProperties()->get()~303getinstance)->contactProperties()->get()getCrmField()A NO~307getinstance()->dealProperties)-getCrmField()A NO>get(~391getinstance()->getClient()-›request('GET', pipelines)syncPipelines()A NO~475getinstance()->getClient()->request('GET'",/integrations/v1/me)syncConfig®)A NO~495client->getOwners()syncProfiles()A NO~601client-›search('companies'/'contacts,find) - 2x search calls• YES~697~813client->getPaginatedData(Spayload,deas)client-syncOpportunities()V YES (via pagination)getTasks() - 2x calls• YESA100% CFri 8 May 17:11:11Edited just now8 Share ~* ...MtttlRate Limit4 BURST4 BURST4 BURST4 BURST4 BURST4 BURST• SEARCH• SEARCH• SEARCH...
|
NULL
|
-6864039401780154302
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelp‹ 40lth)Lukás K NotionFileEditViewHistoryWindowHelp‹ 40lth)Lukás Koválik's No...ProGF+n Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgexe IdeasPrivateE Home viewsHubspot API callsWork KnowledgeIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x03 Hubspot / Hubspot API callsgetMeeting0)getAssociationsData()A NOANO4 BURST4 BURSTaddAssociations() →makeRequest()A NO4 BURSTremoveAssociations() →makeRequest()A NO4 BURSTKey finding: Only search 0 passes through execute Re quest). Every other Client,php method bypasses it.Service.php - All Call SitesLineCallMethod context~299getCrmField()Via executeRequest()?A NOgetinstance)->companyProperties()->get()~303getinstance)->contactProperties()->get()getCrmField()A NO~307getinstance()->dealProperties)-getCrmField()A NO>get(~391getinstance()->getClient()-›request('GET', pipelines)syncPipelines()A NO~475getinstance()->getClient()->request('GET'",/integrations/v1/me)syncConfig®)A NO~495client->getOwners()syncProfiles()A NO~601client-›search('companies'/'contacts,find) - 2x search calls• YES~697~813client->getPaginatedData(Spayload,deas)client-syncOpportunities()V YES (via pagination)getTasks() - 2x calls• YESA100% CFri 8 May 17:11:11Edited just now8 Share ~* ...MtttlRate Limit4 BURST4 BURST4 BURST4 BURST4 BURST4 BURST• SEARCH• SEARCH• SEARCH...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10086
|
458
|
22
|
2026-05-08T14:11:19.370074+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249479370_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i (1 Sdeal instancenf NealWithAccociations) «thnow new CrmFycentiond messaae"Neal not found!)•notunn] local.INF0: [Hubspot] DEBUG Getting headers {w19AV2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Relimiting systemWhy line 1313 was missed previously=) Service.php:1313Sresponse = sth1s->cl1ent→>getNewinstance()→>crm()→>contacts()→>searchAp1()→>dosearch(Spay load)=The previous audit searched for sthis->client→> method calls on the Client wrapper class. This line goes directly to the HubSpot SDK via getNewInstance()which returns a raw Discovery object - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files) |Legend! NOT via executeRequest () - 429s silently fall through as raw exceptions* SEARCH rate limit (5 req/sec)BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodaetOpportunityBvIdo)rho May 1/.11.19+0 ..fetchCallDispositionso)tetchopportun1tyPipel1neStagesfetchOpportunityPipelines() → makeRequest()fetchMeetingOutcomeTypes() / fetchCallActivityTypes() → extractMeetingType0ptions()makeRequest() (GET/POST/PATCH)|createMeeting() → makeRequest() |lateMeeting() → makeRequest() |deleteFnaagement ()Ask anvthing (84D)C° AdantiveVia executeRequest()?Rate LimitVYES4 SEARCHNO (cented out) |÷ BURSTI!NO!NOA NOA NOA NONO- BURST÷ BURST9 BURST- BURST, BURSTW Windsurf Teams 238:34 UTF-8 4 spaces...
|
NULL
|
2726139332648314366
|
NULL
|
idle
|
ocr
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i (1 Sdeal instancenf NealWithAccociations) «thnow new CrmFycentiond messaae"Neal not found!)•notunn] local.INF0: [Hubspot] DEBUG Getting headers {w19AV2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Relimiting systemWhy line 1313 was missed previously=) Service.php:1313Sresponse = sth1s->cl1ent→>getNewinstance()→>crm()→>contacts()→>searchAp1()→>dosearch(Spay load)=The previous audit searched for sthis->client→> method calls on the Client wrapper class. This line goes directly to the HubSpot SDK via getNewInstance()which returns a raw Discovery object - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files) |Legend! NOT via executeRequest () - 429s silently fall through as raw exceptions* SEARCH rate limit (5 req/sec)BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodaetOpportunityBvIdo)rho May 1/.11.19+0 ..fetchCallDispositionso)tetchopportun1tyPipel1neStagesfetchOpportunityPipelines() → makeRequest()fetchMeetingOutcomeTypes() / fetchCallActivityTypes() → extractMeetingType0ptions()makeRequest() (GET/POST/PATCH)|createMeeting() → makeRequest() |lateMeeting() → makeRequest() |deleteFnaagement ()Ask anvthing (84D)C° AdantiveVia executeRequest()?Rate LimitVYES4 SEARCHNO (cented out) |÷ BURSTI!NO!NOA NOA NOA NONO- BURST÷ BURST9 BURST- BURST, BURSTW Windsurf Teams 238:34 UTF-8 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10087
|
457
|
25
|
2026-05-08T14:11:19.427197+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249479427_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4 Todo2 New chat x03 Hubspot / Hubspot API callsgetClient()->deleteEngagement()updateTask()getClient()->createEngagement()createActivity )getClient()->updateMeeting0 →makeRequest()updateMeetingV3()getClient()->createMeeting() →makeRequest()createMeetingV3()getClient()->addAssociations() →makeRequest()updateActivityAssociations()getClient()->removeAssociations() →makeRequest()updateActivityAssociations()getClient()->createEngagement()saveFollowupActivity()ServiceTraits/SyncCrmEntitiesTrait.phpCallMethodclient->getAssociationsData0)getAssociationDataForCollection()client->getPaginatedDataGenerator($payload, 'contacts')syncContacts()client->getContactByid0syncSingleContact()client-›getPaginatedDataGenerator(Spayload, 'companies')syncAccounts()client->getAccountByld0syncSing leAccount ()client->getContactsBylds0batchFetchContacts()client->getCompaniesBylds() (x2)batchFetchCompanies/ForAssociations()lualA100% <•Fri 8 May 17:11:20Edited 1m ago8ShareA NOA NOA NOA NOA NOA NOA NO4 BURST4 BURST4 BURST4 BURST4 BURST4 BURST+ BURSTVia executeRequest()?A NOV YESRate Limit4 BURST• SEARCHA NO• YESA NOA NOA NO4 BURST• SEARCH+ BURST4 BURST4 BURST...
|
NULL
|
506521039365857955
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4 Todo2 New chat x03 Hubspot / Hubspot API callsgetClient()->deleteEngagement()updateTask()getClient()->createEngagement()createActivity )getClient()->updateMeeting0 →makeRequest()updateMeetingV3()getClient()->createMeeting() →makeRequest()createMeetingV3()getClient()->addAssociations() →makeRequest()updateActivityAssociations()getClient()->removeAssociations() →makeRequest()updateActivityAssociations()getClient()->createEngagement()saveFollowupActivity()ServiceTraits/SyncCrmEntitiesTrait.phpCallMethodclient->getAssociationsData0)getAssociationDataForCollection()client->getPaginatedDataGenerator($payload, 'contacts')syncContacts()client->getContactByid0syncSingleContact()client-›getPaginatedDataGenerator(Spayload, 'companies')syncAccounts()client->getAccountByld0syncSing leAccount ()client->getContactsBylds0batchFetchContacts()client->getCompaniesBylds() (x2)batchFetchCompanies/ForAssociations()lualA100% <•Fri 8 May 17:11:20Edited 1m ago8ShareA NOA NOA NOA NOA NOA NOA NO4 BURST4 BURST4 BURST4 BURST4 BURST4 BURST+ BURSTVia executeRequest()?A NOV YESRate Limit4 BURST• SEARCHA NO• YESA NOA NOA NO4 BURST• SEARCH+ BURST4 BURST4 BURST...
|
10085
|
NULL
|
NULL
|
NULL
|
|
10088
|
457
|
26
|
2026-05-08T14:11:23.649297+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249483649_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledge /Work Knowledgetesting keyboard4 Todo3 Hubspot / Hubspot API callsgetClient()->addAssociations()makeRequest()updateActivityAssociations()A NOgetClient()->removeAssociations() →makeRequest()updateActivityAssociations()A NOgetClient()->createEngagement()saveFollowupActivity)A NOServiceTraits/SyncCrmEntitiesTrait.phpCallMethodclient->getAssociationsData0getAssociationDataForCollection()client->getPaginatedDataGenerator($payload, 'contacts")syncContacts()client->getContactByld0syncSingleContact()client->getPaginatedDataGenerator($payload, 'companies')syncAccounts()client->getAccountByld)syncSingleAccount()A NOclient->getContactsBylds0batchFetchContacts()A NOclient->getCompaniesBylds0) (x2)batchFetchCompanies/ForAssociations()A NOServiceTraits/OpportunitySyncTrait.phpCallMethodVia executeRequest()?& New chat *0client->getOpportunitiesBylds()syncOpportunitiesBatch()A NOAlontsvncinnartunitjecßatchlANO‹ 40lallA100% <*Edited 1m agoFri 8 May 17:11:258 Share ~* ...4 BURST4 BURST4 BURSTVia executeRequest()?A NOVYESA NOV YESRate Limit4 BURST• SEARCH4 BURST• SEARCH4 BURST4 BURST4 BURSTRate Limit4 BURST& PIIDCT...
|
NULL
|
6991150416642364108
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledge /Work Knowledgetesting keyboard4 Todo3 Hubspot / Hubspot API callsgetClient()->addAssociations()makeRequest()updateActivityAssociations()A NOgetClient()->removeAssociations() →makeRequest()updateActivityAssociations()A NOgetClient()->createEngagement()saveFollowupActivity)A NOServiceTraits/SyncCrmEntitiesTrait.phpCallMethodclient->getAssociationsData0getAssociationDataForCollection()client->getPaginatedDataGenerator($payload, 'contacts")syncContacts()client->getContactByld0syncSingleContact()client->getPaginatedDataGenerator($payload, 'companies')syncAccounts()client->getAccountByld)syncSingleAccount()A NOclient->getContactsBylds0batchFetchContacts()A NOclient->getCompaniesBylds0) (x2)batchFetchCompanies/ForAssociations()A NOServiceTraits/OpportunitySyncTrait.phpCallMethodVia executeRequest()?& New chat *0client->getOpportunitiesBylds()syncOpportunitiesBatch()A NOAlontsvncinnartunitjecßatchlANO‹ 40lallA100% <*Edited 1m agoFri 8 May 17:11:258 Share ~* ...4 BURST4 BURST4 BURSTVia executeRequest()?A NOVYESA NOV YESRate Limit4 BURST• SEARCH4 BURST• SEARCH4 BURST4 BURST4 BURSTRate Limit4 BURST& PIIDCT...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10089
|
458
|
23
|
2026-05-08T14:11:51.270467+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249511270_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i (1 Sdeal instancenf NealWithAccociations) «thnow new CrmFycentiond messaae"Neal not found!)•notunn] local.INF0: [Hubspot] DEBUG Getting headers {w19AV2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Relimiting systemWhy line 1313 was missed previously=) Service.php:1313Sresponse = sth1s->cl1ent→>getNewinstance()→>crm()→>contacts()→>searchAp1()→>dosearch(Spay load)=The previous audit searched for sthis->client→> method calls on the Client wrapper class. This line goes directly to the HubSpot SDK via getNewInstance()which returns a raw Discovery object - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files) |Legend! NOT via executeRequest () - 429s silently fall through as raw exceptions* SEARCH rate limit (5 req/sec)BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodaetOpportunityBvIdo)rhl o May 1/.11:01+0 ..fetchCallDispositionso)tetchopportun1tyPipel1neStagesfetchOpportunityPipelines() → makeRequest()fetchMeetingOutcomeTypes() / fetchCallActivityTypes() → extractMeetingType0ptions()makeRequest() (GET/POST/PATCH)|createMeeting() → makeRequest() |lateMeeting() → makeRequest() |deleteFnaagement ()Ask anvthing (84D)C° AdantiveVia executeRequest()?Rate LimitVYES4 SEARCHNO (cented out) |÷ BURSTI!NO!NOA NOA NOA NONO- BURST÷ BURST9 BURST- BURST, BURSTW Windsurf Teams 238:34 UTF-8 4 spaces...
|
NULL
|
4827521773473629111
|
NULL
|
idle
|
ocr
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]© SyncRelatedActivityManager.phpV syncermenttes tralt.onpA console [STAGING)• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnects sobnectivoer/search"*return Sthis->executeRequest(function O use Sendooint. Soavload) «$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm->deals(->basicApi©->getById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i (1 Sdeal instancenf NealWithAccociations) «thnow new CrmFycentiond messaae"Neal not found!)•notunn] local.INF0: [Hubspot] DEBUG Getting headers {w19AV2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3".cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn30%2BKVA3mFI.:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call Relimiting systemWhy line 1313 was missed previously=) Service.php:1313Sresponse = sth1s->cl1ent→>getNewinstance()→>crm()→>contacts()→>searchAp1()→>dosearch(Spay load)=The previous audit searched for sthis->client→> method calls on the Client wrapper class. This line goes directly to the HubSpot SDK via getNewInstance()which returns a raw Discovery object - it never hits any wrapper method, so it was invisible to that search.Complete Call List (All Files) |Legend! NOT via executeRequest () - 429s silently fall through as raw exceptions* SEARCH rate limit (5 req/sec)BURST rate limit (100-200 req/10s)Client.php - All API MethodsMethodaetOpportunityBvIdo)rhl o May 1/.11:01+0 ..fetchCallDispositionso)tetchopportun1tyPipel1neStagesfetchOpportunityPipelines() → makeRequest()fetchMeetingOutcomeTypes() / fetchCallActivityTypes() → extractMeetingType0ptions()makeRequest() (GET/POST/PATCH)|createMeeting() → makeRequest() |lateMeeting() → makeRequest() |deleteFnaagement ()Ask anvthing (84D)C° AdantiveVia executeRequest()?Rate LimitVYES4 SEARCHNO (cented out) |÷ BURSTI!NO!NOA NOA NOA NONO- BURST÷ BURST9 BURST- BURST, BURSTW Windsurf Teams 238:34 UTF-8 4 spaces...
|
10086
|
NULL
|
NULL
|
NULL
|
|
10090
|
457
|
27
|
2026-05-08T14:11:56.471964+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249516471_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledge /Work Knowledgetesting keyboard4 Todo3 Hubspot / Hubspot API callsgetClient()->addAssociations()makeRequest()updateActivityAssociations()A NOgetClient()->removeAssociations() →makeRequest()updateActivityAssociations()A NOgetClient()->createEngagement()saveFollowupActivity)A NOServiceTraits/SyncCrmEntitiesTrait.phpCallMethodclient->getAssociationsData0getAssociationDataForCollection()client->getPaginatedDataGenerator($payload, 'contacts")syncContacts()client->getContactByld0syncSingleContact()client->getPaginatedDataGenerator($payload, 'companies')syncAccounts()client->getAccountByld)syncSingleAccount()A NOclient->getContactsBylds0batchFetchContacts()A NOclient->getCompaniesBylds0) (x2)batchFetchCompanies/ForAssociations()A NOServiceTraits/OpportunitySyncTrait.phpCallMethodVia executeRequest()?& New chat *0client->getOpportunitiesBylds()syncOpportunitiesBatch()A NOAlontsvncinnartunitjecßatchlANO‹ 40lallA100% <*Edited 1m agoFri 8 May 17:11:568 Share ~* ...4 BURST4 BURST4 BURSTVia executeRequest()?A NOVYESA NOV YESRate Limit4 BURST• SEARCH4 BURST• SEARCH4 BURST4 BURST4 BURSTRate Limit4 BURST& PIIDCT...
|
NULL
|
5705421727105570814
|
NULL
|
idle
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledge /Work Knowledgetesting keyboard4 Todo3 Hubspot / Hubspot API callsgetClient()->addAssociations()makeRequest()updateActivityAssociations()A NOgetClient()->removeAssociations() →makeRequest()updateActivityAssociations()A NOgetClient()->createEngagement()saveFollowupActivity)A NOServiceTraits/SyncCrmEntitiesTrait.phpCallMethodclient->getAssociationsData0getAssociationDataForCollection()client->getPaginatedDataGenerator($payload, 'contacts")syncContacts()client->getContactByld0syncSingleContact()client->getPaginatedDataGenerator($payload, 'companies')syncAccounts()client->getAccountByld)syncSingleAccount()A NOclient->getContactsBylds0batchFetchContacts()A NOclient->getCompaniesBylds0) (x2)batchFetchCompanies/ForAssociations()A NOServiceTraits/OpportunitySyncTrait.phpCallMethodVia executeRequest()?& New chat *0client->getOpportunitiesBylds()syncOpportunitiesBatch()A NOAlontsvncinnartunitjecßatchlANO‹ 40lallA100% <*Edited 1m agoFri 8 May 17:11:568 Share ~* ...4 BURST4 BURST4 BURSTVia executeRequest()?A NOVYESA NOV YESRate Limit4 BURST• SEARCH4 BURST• SEARCH4 BURST4 BURST4 BURSTRate Limit4 BURST& PIIDCT...
|
10088
|
NULL
|
NULL
|
NULL
|
|
10091
|
457
|
28
|
2026-05-08T14:12:02.680341+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249522680_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1ĐTododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4Todo2 New chat x03 Hubspot / Hubspot API callsCallMethodclient->getOpportunitiesBylds0)syncOpportunitiesBatch()client->getAssociationsData('deals",'companies')syncOpportunitiesBatch()client->getAssociationsData('deals",'contacts')syncOpportunitiesBatch()client->getCompaniesBylds() orgetContactsBylds()syncCrmObjectso)ServiceTraits/SyncFieldsTrait.phpCallMethodclient->getinstance()->companyProperties().all()getObjectFields()client->getinstance()->contactProperties.allogetObjectFields()client->getinstance()->dealProperties().all)getObjectFields()client->getinstance)->objectProperties(*call/meeting/task*).allogetObjectFields()client->getinstance)->objectProperties(Stype)->get(Sid)importObjectField)OpportunitySyncStrategy/CallFileA100% <47Edited 1m agoFri 8 May 17:12:038ShareVia executeRequest()?A NOA NOA NOA NORate Limit4 BURST4 BURST4 BURST4 BURSTVia executeRequest()?A NOA NOA NOA NOA NORate Limit, BURST4 BURST4 BURST4 BURST4 BURSTMethodVia executeRequest()?Rate Limit...
|
NULL
|
151780521865664430
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1ĐTododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4Todo2 New chat x03 Hubspot / Hubspot API callsCallMethodclient->getOpportunitiesBylds0)syncOpportunitiesBatch()client->getAssociationsData('deals",'companies')syncOpportunitiesBatch()client->getAssociationsData('deals",'contacts')syncOpportunitiesBatch()client->getCompaniesBylds() orgetContactsBylds()syncCrmObjectso)ServiceTraits/SyncFieldsTrait.phpCallMethodclient->getinstance()->companyProperties().all()getObjectFields()client->getinstance()->contactProperties.allogetObjectFields()client->getinstance()->dealProperties().all)getObjectFields()client->getinstance)->objectProperties(*call/meeting/task*).allogetObjectFields()client->getinstance)->objectProperties(Stype)->get(Sid)importObjectField)OpportunitySyncStrategy/CallFileA100% <47Edited 1m agoFri 8 May 17:12:038ShareVia executeRequest()?A NOA NOA NOA NORate Limit4 BURST4 BURST4 BURST4 BURSTVia executeRequest()?A NOA NOA NOA NOA NORate Limit, BURST4 BURST4 BURST4 BURST4 BURSTMethodVia executeRequest()?Rate Limit...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10092
|
458
|
24
|
2026-05-08T14:12:03.642089+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249523642_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {W19 ^V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'rho May 1/.12:04AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethod+0 ..Via executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?client->get0wnersArchived() → makeRequest()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmservice->searchcaltsrorPer1od), crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance->crm@→>contactso→searchAni→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with cient->search• (different endpoint/format), but could be wraoped in executeRequestol3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
2490469569226172223
|
NULL
|
visual_change
|
ocr
|
NULL
|
Proiect(§)ƠCrmObiectsDecorateActivityDummyD Helper Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›UrielasC) Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function search(string Sobnectivoe, arrav Spavload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {W19 ^V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'rho May 1/.12:04AskJiminnyReportActivityServiceTest -CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethod+0 ..Via executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?client->get0wnersArchived() → makeRequest()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall@),crmservice->searchcaltsrorPer1od), crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance->crm@→>contactso→searchAni→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with cient->search• (different endpoint/format), but could be wraoped in executeRequestol3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10093
|
457
|
29
|
2026-05-08T14:12:12.134069+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249532134_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork KnowledgeHubspot / Hubspot API callsCallMethodclient->getOwnersArchived0 →makeRequest()execute()Work Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Fri 8 May 17:12:13Edited 1m ago8 Share ~...Via executeRequest()?A NORate Limit, BURST+ :I app/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 — raw searchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
NULL
|
1225543548460828689
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork KnowledgeHubspot / Hubspot API callsCallMethodclient->getOwnersArchived0 →makeRequest()execute()Work Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Fri 8 May 17:12:13Edited 1m ago8 Share ~...Via executeRequest()?A NORate Limit, BURST+ :I app/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 — raw searchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
10091
|
NULL
|
NULL
|
NULL
|
|
10094
|
458
|
25
|
2026-05-08T14:12:36.060659+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249556060_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rio May 1/.12.30Proiect(§)ƠCrmObiectsDecorateActiv rio May 1/.12.30Proiect(§)ƠCrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
-3145790862535111403
|
NULL
|
idle
|
ocr
|
NULL
|
rio May 1/.12.30Proiect(§)ƠCrmObiectsDecorateActiv rio May 1/.12.30Proiect(§)ƠCrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
10092
|
NULL
|
NULL
|
NULL
|
|
10095
|
457
|
30
|
2026-05-08T14:12:44.009694+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249564009_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n HomeWork KnowledgeHubspot / Hubspot API calls1000CallMethodTestDailyclient->getOwnersArchived0 →makeRequest()execute()Agents+ New agent+ :Workspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Edited 2m agoFri 8 May 17:12:458 Share ~...Via executeRequest()?A NORate Limit, BURSTJapp/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 - rawsearchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
NULL
|
6228679071037076563
|
NULL
|
idle
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n HomeWork KnowledgeHubspot / Hubspot API calls1000CallMethodTestDailyclient->getOwnersArchived0 →makeRequest()execute()Agents+ New agent+ :Workspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Edited 2m agoFri 8 May 17:12:458 Share ~...Via executeRequest()?A NORate Limit, BURSTJapp/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 - rawsearchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10096
|
NULL
|
0
|
2026-05-08T14:13:07.787032+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249587787_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rilo May 1/.13.09Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.13.09Proiect(§)ƠCrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
2417715602853191282
|
NULL
|
idle
|
ocr
|
NULL
|
rilo May 1/.13.09Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.13.09Proiect(§)ƠCrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10097
|
NULL
|
0
|
2026-05-08T14:13:16.669458+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249596669_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n HomeWork KnowledgeHubspot / Hubspot API calls1000CallMethodTestDailyclient->getOwnersArchived0 →makeRequest()execute()Agents+ New agent+ :Workspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Edited 3m agoFri 8 May 17:13:168 Share ~...Via executeRequest()?A NORate Limit, BURSTJapp/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 - rawsearchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
NULL
|
-5542474743363701325
|
NULL
|
idle
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n HomeWork KnowledgeHubspot / Hubspot API calls1000CallMethodTestDailyclient->getOwnersArchived0 →makeRequest()execute()Agents+ New agent+ :Workspace* Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Edited 3m agoFri 8 May 17:13:168 Share ~...Via executeRequest()?A NORate Limit, BURSTJapp/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 - rawsearchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
10095
|
NULL
|
NULL
|
NULL
|
|
10098
|
460
|
0
|
2026-05-08T14:13:40.858659+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249620858_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rilo May 1/:13.44Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/:13.44Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
6073419002472656113
|
NULL
|
idle
|
ocr
|
NULL
|
rilo May 1/:13.44Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/:13.44Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
10096
|
NULL
|
NULL
|
NULL
|
|
10099
|
459
|
0
|
2026-05-08T14:13:47.978502+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249627978_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n HomeWork KnowledgeHubspot / Hubspot API calls1000CallMethodTestDailyclient->getOwnersArchived0 →makeRequest()execute()Agents+ New agent+ :Workspace& Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Fri 8 May 17:13:48Edited 3m ago8 Share ~...Via executeRequest()?A NORate Limit, BURSTapp/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 — raw searchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
NULL
|
7849363747917671279
|
NULL
|
idle
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n HomeWork KnowledgeHubspot / Hubspot API calls1000CallMethodTestDailyclient->getOwnersArchived0 →makeRequest()execute()Agents+ New agent+ :Workspace& Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Fri 8 May 17:13:48Edited 3m ago8 Share ~...Via executeRequest()?A NORate Limit, BURSTapp/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 — raw searchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10100
|
460
|
1
|
2026-05-08T14:14:14.054785+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249654054_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rio May 1/-14.14Proiect(§)ƠCrmObiectsDecorateActiv rio May 1/-14.14Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
55324180435945773
|
NULL
|
idle
|
ocr
|
NULL
|
rio May 1/-14.14Proiect(§)ƠCrmObiectsDecorateActiv rio May 1/-14.14Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10101
|
459
|
1
|
2026-05-08T14:14:18.838113+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249658838_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n HomeWork KnowledgeHubspot / Hubspot API calls1000CallMethodTestDailyclient->getOwnersArchived0 →makeRequest()execute()Agents+ New agent+ :Workspace& Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Fri 8 May 17:14:19Edited 4m ago8 Share ~...Via executeRequest()?A NORate Limit, BURSTapp/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 — raw searchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
NULL
|
-221151524036781033
|
NULL
|
idle
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...PrcGPHubspot API calls+n HomeWork KnowledgeHubspot / Hubspot API calls1000CallMethodTestDailyclient->getOwnersArchived0 →makeRequest()execute()Agents+ New agent+ :Workspace& Quick Note- WorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgece IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40labl100% <7*Fri 8 May 17:14:19Edited 4m ago8 Share ~...Via executeRequest()?A NORate Limit, BURSTapp/Services/Activity/HubSpot/Service.phpThis service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall(), crmService->searchCallsForPeriod0, crmService->syncOpportunity() , crmService→>parseRecords() .Critical Findings1. Line 1313 — raw searchApi()->doSearch() - matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out:...
|
10099
|
NULL
|
NULL
|
NULL
|
|
10102
|
459
|
2
|
2026-05-08T14:14:42.027315+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249682027_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelp‹ 40labl•Lukás NotionFileEditViewHistoryWindowHelp‹ 40labl•Lukás Koválik's No...ProGPHubspot API callsn Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgeca IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4Todo2 New chat x0Tworoer3 Hubspot / Hubspot API callsPьO Pьlьне суUUтьноносьвьгг Pино Choо coсucpel cuooce cteрay toaura• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()I. getOpportunityByld() has executeRequest() commented out://$deal = $this->executeRequest(fn () = $this->getNewInstance()->crm()->deals()->basicApi()->getById($deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.100% <7*Fri 8 May 17:14:42Edited 4m ago8 Share ~* ......
|
NULL
|
-1192949850693658763
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelp‹ 40labl•Lukás NotionFileEditViewHistoryWindowHelp‹ 40labl•Lukás Koválik's No...ProGPHubspot API callsn Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgeca IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4Todo2 New chat x0Tworoer3 Hubspot / Hubspot API callsPьO Pьlьне суUUтьноносьвьгг Pино Choо coсucpel cuooce cteрay toaura• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()I. getOpportunityByld() has executeRequest() commented out://$deal = $this->executeRequest(fn () = $this->getNewInstance()->crm()->deals()->basicApi()->getById($deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.100% <7*Fri 8 May 17:14:42Edited 4m ago8 Share ~* ......
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10103
|
460
|
2
|
2026-05-08T14:14:43.088581+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249683088_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rio May 1/:14.49Proiect(§)ƠCrmObiectsDecorateActiv rio May 1/:14.49Proiect(§)ƠCrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar257© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
1878210785369377766
|
NULL
|
click
|
ocr
|
NULL
|
rio May 1/:14.49Proiect(§)ƠCrmObiectsDecorateActiv rio May 1/:14.49Proiect(§)ƠCrmObiectsDecorateActivitye DummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile>@ Webhook|c) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar257© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€72764 V1V1A* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3034oAns 0o 00)- 0m 2= 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF"ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
10100
|
NULL
|
NULL
|
NULL
|
|
10104
|
459
|
3
|
2026-05-08T14:14:45.061556+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249685061_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelp‹ 40abl•Lukás K NotionFileEditViewHistoryWindowHelp‹ 40abl•Lukás Koválik's No...PrcGPHubspot API callsn Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgeca IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4Todo2 New chat x0Tworoer3 Hubspot / Hubspot API callsPьO Pьlьне суUUтьноносьвьгг Pино Choо coсucpel cuooce cteрay toaura• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomain• No 429 protectionwhich calls a search endpoint - hits the search rate limit bucket• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out://$deal = $this->executeRequest(fn () = $this->getNewInstance()->crm()->deals()->basicApi()->getById($deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(• Deliberately disabled - this means single deal fetches inHubspotSingleSyncStrategyalso get no 429 protection.100% CFri 8 May 17:14:45Edited 4m ago8 Share ~* ...Te CommentSkillsImprove writingProofreadExplainPeformatEdit with Al...
|
NULL
|
-3707266139218704055
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelp‹ 40abl•Lukás K NotionFileEditViewHistoryWindowHelp‹ 40abl•Lukás Koválik's No...PrcGPHubspot API callsn Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprint|Stefka 1-1ĐTododevEvaluationKnowledgeca IdeasPrivateE Home viewsIntegration-appWork KnowledgeWork Knowledgetesting keyboard4Todo2 New chat x0Tworoer3 Hubspot / Hubspot API callsPьO Pьlьне суUUтьноносьвьгг Pино Choо coсucpel cuooce cteрay toaura• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies ()->searchByDomain() - matchByDomain():$hsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomain• No 429 protectionwhich calls a search endpoint - hits the search rate limit bucket• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()3. getOpportunityByld() has executeRequest() commented out://$deal = $this->executeRequest(fn () = $this->getNewInstance()->crm()->deals()->basicApi()->getById($deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(• Deliberately disabled - this means single deal fetches inHubspotSingleSyncStrategyalso get no 429 protection.100% CFri 8 May 17:14:45Edited 4m ago8 Share ~* ...Te CommentSkillsImprove writingProofreadExplainPeformatEdit with Al...
|
10102
|
NULL
|
NULL
|
NULL
|
|
10105
|
459
|
4
|
2026-05-08T14:14:51.447560+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249691447_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewLukás Koválik's No...History NotionFileEditViewLukás Koválik's No...HistoryWindowHelp‹ 40alal100% CFri 8 May 17:14:51n Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4TodoCritical Findings1. Line 1313 - rawsearchApi ()->doSearch()— matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain ($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()Press 'space' for Al or "l' for commands2 New chat x0...
|
NULL
|
-1982754320619556723
|
NULL
|
visual_change
|
ocr
|
NULL
|
NotionFileEditViewLukás Koválik's No...History NotionFileEditViewLukás Koválik's No...HistoryWindowHelp‹ 40alal100% CFri 8 May 17:14:51n Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4TodoCritical Findings1. Line 1313 - rawsearchApi ()->doSearch()— matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 -companies ()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain ($companyName, $companyProperties);• Uses v1 SDK's searchByDomainwhich calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()Press 'space' for Al or "l' for commands2 New chat x0...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10106
|
460
|
3
|
2026-05-08T14:15:15.065310+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249715065_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rilo May 1/.10.19Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.10.19Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
4571711566019954163
|
NULL
|
idle
|
ocr
|
NULL
|
rilo May 1/.10.19Proiect(§)ƠCrmObiectsDecorateActi rilo May 1/.10.19Proiect(§)ƠCrmObiectsDecorateActivityDummyD HelpersD HubspotD AccountSyncStrategy>@ ActionsD ContactSyncStrategyODTO›Urielas0 Journal_ Metadatav _ Opponuni vsyncstrarecConcerns(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiec(c) HubspotLastModitiecc) -uosootLastModitier(c) Huosootsindlesvnes© HubspotSyncStrategC) HubsnotWebhookBav M Padination(C) HubsootPadinationS© PaginationConfig.php© PaginationState.php> • ProspectSearchStrategy) M Pedic|v M ServiceTraitsT OpportunitySyncTraiT) SuncCrmEntitiecTraitT SyncFieldsTrait.phpWriteCrmTrait.phpMtttile> 0 Webhookc) BatchsyncCollector.phpc) BatchSyncRedisServicec) ClosedDealStagesServicDealFieldsService.php(C) DecorateActiviv.onocFieldDerinitions.onoC) FieldivoeConverter.ohc2472481) Hubsootclientinterface.(C) HubsootTokenManaderC) PavloadBuilder.ohvC) RemoteCrmObiectManirga) ResnonseNormalize nhr(c) Service nhn© SyncFieldAction.php(C) SvncPolatedActivitvMar© SyncRelatedActivityManager.phpV syncermenttes tralt.onp• DeleteCrmEntityTrait.php© CheckAndRetryRemoteMatch.php© MatchActivityCrmData© PaginationState.phpC) MatchCrmData.php© ProviderRateLimiter.php(C) PaqinationContia.php2181220 G22722822923€* othrows HubspotException on APl errorgpubuac function searchistrina Sobnectivoe, array Spayload): arravSendoonnt = self::BASE URL , "crm/vs/obnectssobnectivoer/search"*return Sthis->executeReauest(function O use Sendooint. Spavload) <$response = $this->getInstance()->getClient()->request( method: 'POST', $etoreturn $response->toArrayO:!T20E23* Othnows DealApiException* Othnows CrmExceptionЕ2627public function getOpportunityById(string $crmId, array $fields): array29tryfSdeal = Sthis->executeReaSdeal = Sthis->getNewInstance->crm((->deals(->basicApi©->qetById(3e6) -30ra 0) ->683158-3083oAns (0 00)- 0m 2=- 31Scrmld,1mpLode separator: ".', Stlelds)} catch (DealAniException $e) {Sthis->loa->info('[Hubspotl Failed to fetch opportunity'. ['reason' => $e->aetMessage@1):i4 (1 Sdeal instancenf NealWithAccociations)«thnow new CrmFycentiond messace"Neal not found!)•notunn1111 1: = custom.log x = laravel.logA SF [jiminny@localhost]« HS_local [jiminny@localhost]& console [PRODI« console (EU]A console [STAGING)] local.INF0: [Hubspot] DEBUG Getting headers {w19 .V2026 14:21:15 GMT"]oLicacion/son.charser=utt-o,:"chunked"dboodcsa-SoF""DYNAMIC"J.ecurity":L"max-age=31536000; incLudesubbomains; preload"J,hcid; desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",ation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7.cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZEXDZyHz2mBUFd15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZZzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ:0.01,e"1}}{236535-ec98-4541-b92a-adfa73b69eab",-905t-4604-9405-0e50551e5545'CascadeHubspot Rate LimitindHubspot Rate Limit ReInvestigating Rate LinNew CascadeHubspot CRM Call ReCallMethodVia executeRequest()?getEngagementOptions()client→>fetchOpportunityPipelineStages(getOoportunity0otionso|Actions/SyncArchivedProfilesAction.phpCallVia executeRequest()?Rate Limitclient->get0wnersArchived() → makeRequest()execute()This service uses $this->crmService (the CRM Service.php) - it makes no direct HubSpot API calls itself. All calls delegate through crmService->getCall),crmservice->searchcautsrorPer10d, crmservice->syncopportunity, crmservice->parseRecords.Critical Findings1. Line 1313 — raw searchAni()->doSearch() -matchEyactlvBvParticioant()inService.ohos• Service.php:1313Sresponse = Sthis->client->getNewInstance@=>crm@→>contacts@→searchAoi→doSearch(Soavload):• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with $this->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():# Service.php:920-923ShsAccounts = $this->client-›getInstance()->companies()->searchBvDoma in(ScompanvName, ScompanyProperties):• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client-›search() (different endpoint/format), but could be wrapped in executeRequest ()3. getOpportunityById() has executeRequest() commented out:•Client.php:238-239$deal = Sthis->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = Sthis->aetNewInstance->crmo->deals@→>basicApi->aetBvTd• Deliberately disabled - this means single deal fetches in HubspotSingleSyncStrategy also get no 429 protection.feunl «eAsk anvthina (&4L)+ « Codefo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10107
|
459
|
5
|
2026-05-08T14:15:22.268955+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249722268_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelp•Lukás Koválik& NotionFileEditViewHistoryWindowHelp•Lukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledge3 Hubspot / Hubspot API callsWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40alol100% <7*Fri 8 May 17:15:22Edited just now8 Share ~Critical Findings1. Line 1313 - rawsearchApi()->doSearch()- matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, noRateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()Press 'space' for Al or '/' for commands...
|
NULL
|
6444292959860202566
|
NULL
|
idle
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelp•Lukás Koválik& NotionFileEditViewHistoryWindowHelp•Lukás Koválik's No...PrcGPHubspot API calls+n Home1000TestDailyAgents+ New agentWorkspace& Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgexe IdeasPrivateE Home viewsIntegration-appWork Knowledge3 Hubspot / Hubspot API callsWork Knowledgetesting keyboard4Todo2 New chat x0‹ 40alol100% <7*Fri 8 May 17:15:22Edited just now8 Share ~Critical Findings1. Line 1313 - rawsearchApi()->doSearch()- matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, noRateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()Press 'space' for Al or '/' for commands...
|
10105
|
NULL
|
NULL
|
NULL
|
|
10108
|
459
|
6
|
2026-05-08T14:15:23.664025+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249723664_m1.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...ProGPWork KnowledgeHubspot API calls+3 Hubspot / Hubspot API callsalol100% [8 Fri 8 May 17:15:27n Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgera IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0Edited just now8 Share ~Critical Findings1. Line 1313 - raw searchApi()->doSearch()- matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()Press 'space' for Al or 'l' for commands...
|
NULL
|
-150027430850240890
|
NULL
|
click
|
ocr
|
NULL
|
NotionFileEditViewHistoryWindowHelpLukás Koválik NotionFileEditViewHistoryWindowHelpLukás Koválik's No...ProGPWork KnowledgeHubspot API calls+3 Hubspot / Hubspot API callsalol100% [8 Fri 8 May 17:15:27n Home1000TestDailyAgents+ New agentWorkspace* Quick NoteWorkJira ticketView of SprintDailyPlanSprintStefka 1-1TododevEvaluationKnowledgera IdeasPrivateE Home viewsIntegration-appWork Knowledgetesting keyboard4Todo2 New chat x0Edited just now8 Share ~Critical Findings1. Line 1313 - raw searchApi()->doSearch()- matchExactlyByParticipant() in Service.php:Sresponse = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);• Goes completely around the Client wrapper - no 429 handling, no RateLimitException, no rate limit awareness• Uses the search rate limit bucket• Can be replaced with Sthis->client->search('contacts', $payload) - same endpoint, same result, adds 429 protection2. Line 920-923 — companies()->searchByDomain() - matchByDomain():ShsAccounts = $this->client->getInstance()->companies()->searchByDomain($companyName, $companyProperties);• Uses v1 SDK's searchByDomain which calls a search endpoint - hits the search rate limit bucket• No 429 protection• Cannot be trivially replaced with client->search() (different endpoint/format), but could be wrapped in executeRequest()Press 'space' for Al or 'l' for commands...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10109
|
460
|
4
|
2026-05-08T14:15:25.498054+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249725498_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
Search
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
0 results
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.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":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"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.64827126,"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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.43018618,"top":0.09736632,"width":0.56981385,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.43018618,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.43018618,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.43018618,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.43018618,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.43018618,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.43018618,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.43018618,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.43018618,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.43018618,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.43018618,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.43018618,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.43018618,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.43018618,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.43018618,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.43018618,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.43018618,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.43018618,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.43018618,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.43018618,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.43018618,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.43018618,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.43018618,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.43018618,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.43018618,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"bounds":{"left":0.10472074,"top":0.17956904,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.11735372,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Search","depth":4,"bounds":{"left":0.12832446,"top":0.17877094,"width":0.043882977,"height":0.015961692},"on_screen":true,"help_text":"Match case","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.18118352,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.19115691,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.19980054,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.20844415,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"0 results","depth":4,"bounds":{"left":0.22207446,"top":0.17797287,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.24767287,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"bounds":{"left":0.25631648,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"bounds":{"left":0.2649601,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.27360374,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5774902097220131525
|
-2472445140285531190
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
Search
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
0 results
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight...
|
10106
|
NULL
|
NULL
|
NULL
|
|
10110
|
459
|
7
|
2026-05-08T14:15:41.571525+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249741571_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"doSearch","depth":4,"on_screen":true,"value":"doSearch","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5516800432225513958
|
-2904790704783406646
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History...
|
10108
|
NULL
|
NULL
|
NULL
|
|
10111
|
460
|
5
|
2026-05-08T14:15:43.267007+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249743267_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.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":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"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.64827126,"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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.43018618,"top":0.09736632,"width":0.56981385,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.43018618,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.43018618,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.43018618,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.43018618,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.43018618,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.43018618,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.43018618,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.43018618,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.43018618,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.43018618,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.43018618,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.43018618,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.43018618,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.43018618,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.43018618,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.43018618,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.43018618,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.43018618,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.43018618,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.43018618,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.43018618,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.43018618,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.43018618,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.43018618,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"bounds":{"left":0.10472074,"top":0.17956904,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.11735372,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"doSearch","depth":4,"bounds":{"left":0.12832446,"top":0.17877094,"width":0.043882977,"height":0.015961692},"on_screen":true,"value":"doSearch","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.18118352,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.19115691,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.19980054,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.20844415,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6679187012828268168
|
-3046671719591638582
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10112
|
460
|
6
|
2026-05-08T14:15:49.569903+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249749569_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/1
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.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":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"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.64827126,"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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.43018618,"top":0.09736632,"width":0.56981385,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.43018618,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.43018618,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.43018618,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.43018618,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.43018618,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.43018618,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.43018618,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.43018618,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.43018618,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.43018618,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.43018618,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.43018618,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.43018618,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.43018618,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.43018618,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.43018618,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.43018618,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.43018618,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.43018618,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.43018618,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.43018618,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.43018618,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.43018618,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.43018618,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"bounds":{"left":0.10472074,"top":0.17956904,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.11735372,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"doSearch","depth":4,"bounds":{"left":0.12832446,"top":0.17877094,"width":0.043882977,"height":0.015961692},"on_screen":true,"value":"doSearch","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.18118352,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.19115691,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.19980054,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.20844415,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/1","depth":4,"bounds":{"left":0.22207446,"top":0.17797287,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.24767287,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"bounds":{"left":0.25631648,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"bounds":{"left":0.2649601,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.27360374,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.39793882,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
80538983116765851
|
-3053145644055950390
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/1
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes...
|
10111
|
NULL
|
NULL
|
NULL
|
|
10113
|
459
|
8
|
2026-05-08T14:16:17.312700+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249777312_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/1
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string TYPE_NOTE = 'NOTE';
private const string TYPE_MEETING = 'MEETING';
private const string TYPE_CALL = 'CALL';
private const string API_URL = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$payload = $this->generateNameSearchPayload($name, $offset, $count);
$type = $objectType === 'companies' ? 'account' : 'contact';
try {
$response = $this->client->search($objectType, $payload);
// Build mapped list.
foreach ($response['results'] as $object) {
$properties = $object['properties'];
$objectName = $this->buildContactName($properties);
$record = [
'crmId' => $object['id'],
// Pass crmUrl to the FE, needed for success message in the extension when you log activity.
'crmUrl' => $this->generateProviderUrl($object['id'], $type),
'name' => $objectName,
'prospectType' => $type,
'phoneNumbers' => [],
];
if ($type === 'account') {
$record['industry'] = $properties['industry'] ?? null;
} else {
$record['title'] = $properties['jobtitle'] ?? null;
$record['organization'] = $properties['company'] ?? null;
}
$countryCode = $this->buildContactCountry($properties);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
// Add phone number to record.
if (! empty($parsedNumber['phone'])) {
$record['phoneNumbers'][] = [
'number' => $parsedNumber['phone'],
'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),
'type' => 'phone',
];
}
// Add mobile phone number to record.
if (! empty($properties['mobilephone'])) {
$mobileNumber = phone_e164($countryCode, $properties['mobilephone']);
if ($mobileNumber !== null) {
$record['phoneNumbers'][] = [
'number' => $mobileNumber,
'nationalFormat' => phone_national($countryCode, $mobileNumber),
'type' => 'mobile',
];
}
}
$data[] = $record;
}
} catch (BadRequest $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->getUuid(),
'request' => $payload,
'reason' => $e->getMessage(),
]);
throw $e;
}
}
return $data;
},
);
}
/**
* @inheritdoc
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
try {
return $this->client->search('contacts', $payload);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'phone' => $phone,
'reason' => $exception->getMessage(),
]);
return [];
}
}
private function normalizePhoneNumber(string $phone): string
{
return ltrim(phone_e164(null, $phone), '+0');
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"doSearch","depth":4,"on_screen":true,"value":"doSearch","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"7","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"48","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->search($objectType, $payload);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n try {\n return $this->client->search('contacts', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n\n return $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n\n return null;\n }\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->search($objectType, $payload);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n try {\n return $this->client->search('contacts', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n\n return $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n\n return null;\n }\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"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}]...
|
-3419229447380999607
|
-465194999110035353
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/1
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string TYPE_NOTE = 'NOTE';
private const string TYPE_MEETING = 'MEETING';
private const string TYPE_CALL = 'CALL';
private const string API_URL = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$payload = $this->generateNameSearchPayload($name, $offset, $count);
$type = $objectType === 'companies' ? 'account' : 'contact';
try {
$response = $this->client->search($objectType, $payload);
// Build mapped list.
foreach ($response['results'] as $object) {
$properties = $object['properties'];
$objectName = $this->buildContactName($properties);
$record = [
'crmId' => $object['id'],
// Pass crmUrl to the FE, needed for success message in the extension when you log activity.
'crmUrl' => $this->generateProviderUrl($object['id'], $type),
'name' => $objectName,
'prospectType' => $type,
'phoneNumbers' => [],
];
if ($type === 'account') {
$record['industry'] = $properties['industry'] ?? null;
} else {
$record['title'] = $properties['jobtitle'] ?? null;
$record['organization'] = $properties['company'] ?? null;
}
$countryCode = $this->buildContactCountry($properties);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
// Add phone number to record.
if (! empty($parsedNumber['phone'])) {
$record['phoneNumbers'][] = [
'number' => $parsedNumber['phone'],
'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),
'type' => 'phone',
];
}
// Add mobile phone number to record.
if (! empty($properties['mobilephone'])) {
$mobileNumber = phone_e164($countryCode, $properties['mobilephone']);
if ($mobileNumber !== null) {
$record['phoneNumbers'][] = [
'number' => $mobileNumber,
'nationalFormat' => phone_national($countryCode, $mobileNumber),
'type' => 'mobile',
];
}
}
$data[] = $record;
}
} catch (BadRequest $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->getUuid(),
'request' => $payload,
'reason' => $e->getMessage(),
]);
throw $e;
}
}
return $data;
},
);
}
/**
* @inheritdoc
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
try {
return $this->client->search('contacts', $payload);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'phone' => $phone,
'reason' => $exception->getMessage(),
]);
return [];
}
}
private function normalizePhoneNumber(string $phone): string
{
return ltrim(phone_e164(null, $phone), '+0');
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10114
|
460
|
7
|
2026-05-08T14:16:22.219337+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778249782219_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/1
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string TYPE_NOTE = 'NOTE';
private const string TYPE_MEETING = 'MEETING';
private const string TYPE_CALL = 'CALL';
private const string API_URL = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$payload = $this->generateNameSearchPayload($name, $offset, $count);
$type = $objectType === 'companies' ? 'account' : 'contact';
try {
$response = $this->client->search($objectType, $payload);
// Build mapped list.
foreach ($response['results'] as $object) {
$properties = $object['properties'];
$objectName = $this->buildContactName($properties);
$record = [
'crmId' => $object['id'],
// Pass crmUrl to the FE, needed for success message in the extension when you log activity.
'crmUrl' => $this->generateProviderUrl($object['id'], $type),
'name' => $objectName,
'prospectType' => $type,
'phoneNumbers' => [],
];
if ($type === 'account') {
$record['industry'] = $properties['industry'] ?? null;
} else {
$record['title'] = $properties['jobtitle'] ?? null;
$record['organization'] = $properties['company'] ?? null;
}
$countryCode = $this->buildContactCountry($properties);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
// Add phone number to record.
if (! empty($parsedNumber['phone'])) {
$record['phoneNumbers'][] = [
'number' => $parsedNumber['phone'],
'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),
'type' => 'phone',
];
}
// Add mobile phone number to record.
if (! empty($properties['mobilephone'])) {
$mobileNumber = phone_e164($countryCode, $properties['mobilephone']);
if ($mobileNumber !== null) {
$record['phoneNumbers'][] = [
'number' => $mobileNumber,
'nationalFormat' => phone_national($countryCode, $mobileNumber),
'type' => 'mobile',
];
}
}
$data[] = $record;
}
} catch (BadRequest $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->getUuid(),
'request' => $payload,
'reason' => $e->getMessage(),
]);
throw $e;
}
}
return $data;
},
);
}
/**
* @inheritdoc
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
try {
return $this->client->search('contacts', $payload);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'phone' => $phone,
'reason' => $exception->getMessage(),
]);
return [];
}
}
private function normalizePhoneNumber(string $phone): string
{
return ltrim(phone_e164(null, $phone), '+0');
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.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":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"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.64827126,"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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.43018618,"top":0.09736632,"width":0.56981385,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.43018618,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.43018618,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.43018618,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.43018618,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.43018618,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.43018618,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.43018618,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.43018618,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.43018618,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.43018618,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.43018618,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.43018618,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.43018618,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.43018618,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.43018618,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.43018618,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.43018618,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.43018618,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.43018618,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.43018618,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.43018618,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.43018618,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.43018618,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.43018618,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"bounds":{"left":0.10472074,"top":0.17956904,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.11735372,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"doSearch","depth":4,"bounds":{"left":0.12832446,"top":0.17877094,"width":0.043882977,"height":0.015961692},"on_screen":true,"value":"doSearch","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.18118352,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.19115691,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.19980054,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.20844415,"top":0.17877094,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/1","depth":4,"bounds":{"left":0.22207446,"top":0.17797287,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.24767287,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"bounds":{"left":0.25631648,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"bounds":{"left":0.2649601,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.27360374,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.39793882,"top":0.17717478,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.34408244,"top":0.20830008,"width":0.0076462766,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"48","depth":4,"bounds":{"left":0.3537234,"top":0.20830008,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.36602393,"top":0.20830008,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.37533244,"top":0.20830008,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.20830008,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39660904,"top":0.20670392,"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.4039229,"top":0.20670392,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->search($objectType, $payload);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n try {\n return $this->client->search('contacts', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n\n return $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n\n return null;\n }\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->search($objectType, $payload);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n try {\n return $this->client->search('contacts', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n\n return $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n\n return null;\n }\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->search('calls', $payload);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"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}]...
|
-3419229447380999607
|
-465194999110035353
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Show Replace Field
Search History
doSearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/1
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Code changed:
Hide
Sync Changes
Hide This Notification
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string TYPE_NOTE = 'NOTE';
private const string TYPE_MEETING = 'MEETING';
private const string TYPE_CALL = 'CALL';
private const string API_URL = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$payload = $this->generateNameSearchPayload($name, $offset, $count);
$type = $objectType === 'companies' ? 'account' : 'contact';
try {
$response = $this->client->search($objectType, $payload);
// Build mapped list.
foreach ($response['results'] as $object) {
$properties = $object['properties'];
$objectName = $this->buildContactName($properties);
$record = [
'crmId' => $object['id'],
// Pass crmUrl to the FE, needed for success message in the extension when you log activity.
'crmUrl' => $this->generateProviderUrl($object['id'], $type),
'name' => $objectName,
'prospectType' => $type,
'phoneNumbers' => [],
];
if ($type === 'account') {
$record['industry'] = $properties['industry'] ?? null;
} else {
$record['title'] = $properties['jobtitle'] ?? null;
$record['organization'] = $properties['company'] ?? null;
}
$countryCode = $this->buildContactCountry($properties);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
// Add phone number to record.
if (! empty($parsedNumber['phone'])) {
$record['phoneNumbers'][] = [
'number' => $parsedNumber['phone'],
'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),
'type' => 'phone',
];
}
// Add mobile phone number to record.
if (! empty($properties['mobilephone'])) {
$mobileNumber = phone_e164($countryCode, $properties['mobilephone']);
if ($mobileNumber !== null) {
$record['phoneNumbers'][] = [
'number' => $mobileNumber,
'nationalFormat' => phone_national($countryCode, $mobileNumber),
'type' => 'mobile',
];
}
}
$data[] = $record;
}
} catch (BadRequest $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->getUuid(),
'request' => $payload,
'reason' => $e->getMessage(),
]);
throw $e;
}
}
return $data;
},
);
}
/**
* @inheritdoc
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
try {
return $this->client->search('contacts', $payload);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'phone' => $phone,
'reason' => $exception->getMessage(),
]);
return [];
}
}
private function normalizePhoneNumber(string $phone): string
{
return ltrim(phone_e164(null, $phone), '+0');
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
...
|
NULL
|
NULL
|
NULL
|
NULL
|