|
4921
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37...
|
4921
|
|
4920
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
Forms
Forms
Components
Components
Development
Development
Code
Code
8 more tabs
More
8
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page
Filter by assignee
Filter assignees by Lukas Kovalik
Filter assignees by Aneliya Angelova
Filter assignees by Nikolay Ivanov
Filter assignees by Nikolay Nikolov
Filter assignees by Steliyan Georgiev
Filter assignees by Unassigned
Epic
Epic
Type
Type
Quick filters
Quick filters
Complete sprint
Complete sprint
Sprint details
Sprint details
Group by Queries
Group
: Queries
Sprint insights
Sprint insights
View settings
View settings
More actions
More actions
Ready For DEV
READY FOR DEV
1
JY-19951 Setup test coverage for Prophet in Sonar. Use the enter key to load the work item.
Setup test coverage for Prophet in Sonar
MAINTENANCE
Backlog
JY-19951
JY-19951
1
In DEV
IN DEV...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37...
|
4920
|
|
4919
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
Forms
Forms
Components
Components
Development
Development
Code
Code
8 more tabs
More
8
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page
Filter by assignee
Filter assignees by Lukas Kovalik
Filter assignees by Aneliya Angelova
Filter assignees by Nikolay Ivanov
Filter assignees by Nikolay Nikolov
Filter assignees by Steliyan Georgiev
Filter assignees by Unassigned
Epic
Epic
Type
Type
Quick filters
Quick filters
Complete sprint
Complete sprint
Sprint details
Sprint details
Group by Queries
Group
: Queries
Sprint insights
Sprint insights
View settings
View settings
More actions
More actions
Ready For DEV
READY FOR DEV
1
JY-19951 Setup test coverage for Prophet in Sonar. Use the enter key to load the work item.
Setup test coverage for Prophet in Sonar
MAINTENANCE
Backlog
JY-19951
JY-19951
1
In DEV
IN DEV
5
JY-20493 Smart Instant Nudge Pre-filtering. Use the enter key to load the work item.
Smart Instant Nudge Pre-filtering
COST-EFFECTIVE AND FASTER NUDGES
In Dev
JY-20493
JY-20493
3.5
pull request
JY-20566 AI Review - Q1 - Summary/Action items/Key Points. Use the enter key to load the work item.
AI Review - Q1 - Summary/Action items/Key Points
GROWTH - MAINTAIN OUR COMPETITIVE POSITION
In Dev
JY-20566
JY-20566
2
Successful deployment to production.
JY-20625 [POC]Jiminny MCP Connector. Use the enter key to load the work item.
[POC]Jiminny MCP Connector
JIMINNY MCP CONNECTOR
In Progress
JY-20625
JY-20625
10
JY-20361 AJ Panorama for Call Scoring in OD. Use the enter key to load the work item.
AJ Panorama for Call Scoring in OD
AUTOMATED AI SCORING
In Dev
JY-20361
JY-20361
2.5
JY-20725 [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts. Use the enter key to load the work item.
[HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts
PLATFORM STABILITY
In Dev
JY-20725
JY-20725
4
Code Review
CODE REVIEW
Create work item in Code Review
Create
Blocked
BLOCKED
Create work item in Blocked
Create
QA
QA
1
Create work item
JY-20352 Sync opportunities without a local owner (user_id is null). Use the enter key to load the work item.
Sync opportunities without a local owner (user_id is null)
PLATFORM STABILITY
In QA
JY-20352
JY-20352
3
pull request
Create work item in QA
Create
PO Acceptance
PO ACCEPTANCE
Create work item in PO Acceptance...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37...
|
4919
|
|
4918
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
Forms
Forms
Components
Components
Development
Development
Code
Code
8 more tabs
More
8
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page
Filter by assignee
Filter assignees by Lukas Kovalik
Filter assignees by Aneliya Angelova
Filter assignees by Nikolay Ivanov
Filter assignees by Nikolay Nikolov
Filter assignees by Steliyan Georgiev
Filter assignees by Unassigned
Epic
Epic
Type
Type
Quick filters
Quick filters
Complete sprint
Complete sprint
Sprint details
Sprint details
Group by Queries
Group
: Queries
Sprint insights
Sprint insights
View settings
View settings
More actions
More actions
Ready For DEV
READY FOR DEV
1
JY-19951 Setup test coverage for Prophet in Sonar. Use the enter key to load the work item.
Setup test coverage for Prophet in Sonar
MAINTENANCE
Backlog
JY-19951
JY-19951
1
In DEV
IN DEV
5
JY-20493 Smart Instant Nudge Pre-filtering. Use the enter key to load the work item.
Smart Instant Nudge Pre-filtering
COST-EFFECTIVE AND FASTER NUDGES
In Dev
JY-20493
JY-20493
3.5
pull request...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37...
|
4918
|
|
4917
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37...
|
4917
|
|
4916
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37?selectedIssue=JY-20725...
|
4916
|
|
4915
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
12 more tabs
More
9+
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37?selectedIssue=JY-20725...
|
4915
|
|
4914
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37?selectedIssue=JY-20725...
|
4914
|
|
4913
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37?selectedIssue=JY-20725...
|
4913
|
|
4912
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Close tab
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to main content
Skip to main content
Toggle organization menu
Issues
Issues
Explore
Explore
Dashboards
Dashboards
Monitors
Monitors
Settings
Settings
Try Business
What's New
Help
[EMAIL]
Issues
Expand
Feed
Feed
Errors & Outages
Errors & Outages
Breached Metrics
Breached Metrics
Warnings
Warnings
User Feedback
User Feedback
Autofix
Autofix
Recently Run
Recently Run
All Views
All Views
Configure
Alerts Moved
Alerts
Moved
Issues
Issues
View Project Details
APP-1EED
Ask Seer
Ask Seer
/
Give Feedback
SevenShores\Hubspot\Exceptions\BadRequest
View events
Events (total)
Users (90d)
Level: Error
Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...)
17K
0
Ongoing
/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::executeSearchRequest
Resolve
Resolve
More resolve options
Archive
Archive
Archive options
Subscribe...
|
Firefox
|
SevenShores\Hubspot\Exceptions\BadRequest: Client SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT — Work...
|
jiminny.sentry.io/issues/7007366572/?environment=p jiminny.sentry.io/issues/7007366572/?environment=production&environment=production-eu&project=82419&query=is%3Aunresolved&referrer=issue-stream&sort=freq...
|
4912
|
|
4911
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Close tab
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to main content
Skip to main content
Toggle organization menu
Issues
Issues
Explore
Explore
Dashboards
Dashboards
Monitors
Monitors
Settings
Settings
Try Business
What's New
Help
[EMAIL]
Issues
Expand
Feed
Feed
Errors & Outages
Errors & Outages
Breached Metrics
Breached Metrics
Warnings
Warnings
User Feedback
User Feedback
Autofix
Autofix
Recently Run
Recently Run
All Views
All Views
Configure
Alerts Moved
Alerts
Moved
Issues
Issues
View Project Details
APP-1EED
Ask Seer
Ask Seer
/
Give Feedback
SevenShores\Hubspot\Exceptions\BadRequest
View events
Events (total)
Users (90d)
Level: Error
Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...)
17K
0
Ongoing
/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::executeSearchRequest
Resolve
Resolve
More resolve options
Archive
Archive
Archive options
Subscribe
Share
More Actions
Priority
Modify issue priority
High
Assignee
Modify issue assignee
Lukas Kovalik
production, production-eu
production, production-eu
90D
90D
Add a search term
Add a search term
Close sidebar
Toggle graph series - Events
Events
17K
Toggle graph series - Users
Users
0
release 68% 874599
release
68%
874599
environment 92% production
environment
92%
production
os.name 100% Linux
os.name
100%
Linux
runtime 94% php 8.3.30
runtime
94%
php 8.3.30
View all tags
View all tags
Select issue content
Events
Previous Event
Next Event
First
First
First
Latest
Latest
Latest
Recommended
Recommended
View More Events
View More Events
Copy as
Copy as
ID: b2e90a6e
14 hours ago
JSON
JSON
Highlights
Highlights
Stack Trace
Stack Trace
Trace
Trace
Tags
Tags
Context
Context
php
8.3.30
Linux
6.1.164-196.303.amzn2023.aarch64
882306...
|
Firefox
|
SevenShores\Hubspot\Exceptions\BadRequest: Client SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT — Work...
|
jiminny.sentry.io/issues/7007366572/?environment=p jiminny.sentry.io/issues/7007366572/?environment=production&environment=production-eu&project=82419&query=is%3Aunresolved&referrer=issue-stream&sort=freq...
|
4911
|
|
4910
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$headers = $response->getHeaders();
$this->logger->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HS_local [jiminny@localhost]
|
NULL
|
4910
|
|
4909
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$headers = $response->getHeaders();
$this->logger->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project...
|
PhpStorm
|
faVsco.js – HS_local [jiminny@localhost]
|
NULL
|
4909
|
|
4908
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
Lukas Kovalik
5:38
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Reducing noise
Firefox FileEdit→ CViewHistoryBookmarksProfilesToolsWindowHelp(ah|= [ meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% <478 • Thu 7 May 17:38:31+Stefka StoyanovaLukas Kovalik5:38 PM | Lukas/Stefka 121• 7:53Lộ3...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4908
|
|
4907
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
Lukas Kovalik
5:38
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Reducing noise
PhostormINavicatecodeFV faVsco.js?9 master kProiectT DeleteCrmEntity lratt.onpCrmObiects© RateLimitException.php© HubspotPaginationService.php X ©PaginationState.php> • DecorateActivityDummy> • Helpers© Hubs©) Hubspotsinglesyncstrategy.phpv D HubspotAccountSyncStrateHubspotwebhookbatchsyncstrateav.ongmoortopportunity8atch.ono>@ ActionsImportBatchJobTrait.pho(c) Hubspot/Service.php() PayloadBuilder.phpC) Companies.php© MatchActivityCrmData.php_ contactsyncstrateC) CrmActivityService.pho(C) CachedCrmServiceDecorator.php•Hubspot/....SyncCrmEntitiesTrait.phpC) Pipedrive/Service.php•DDIe1Servicelnterface.oho@ OpportunitvSvncTest.ohn• U rielas• W Journa.cllass HuhsnotPaoinationServilceA19 V1 лMetadataOpponunilvsyncsuu Concerns(C) HubspotlastMoi 133(C) HubspotlastMoi 134© HubspotLastMor 139(C) HuospotlastMo(C) —uosootLastMot 134(C) Huosootsindlest 154C. HubsnotSvncStr(C) HubsnotWebhoc 155)v Padination(c) PadinationConfit 143)(c) DadinationState 14/m ProcnectSoarchStr: 14g• m Pedic• D ServiceTraitsT. OpportunitySync 168T SyncCrmEntities 169T SyncFieldsTrait.| 17€T WriteCrmTrait.pl 171> O Utils|> 0 Webhook|c) BatchSyncCollectol 174C) BatchSyncRedisSer 179c) Client.phpC) ClosedDealStagess 177CDealFieldsService.c 178C) DecorateActiviv.or 176c)FieldDerinitions.onr 180C) FieldivoeConvertel 18.1) HubsootClientinterl 182C) HubsootTokenMan: 183© PayloadBuilder.php 184C) RemoteCrmObiecti 1899 ResnonseNormalize 194(c) Service nhn(C) SvncFioldAction nh 199(C) SvncRelatedActiviti 190private function handlePaginationStrategy(return Soavload:lusageprivate function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool{.:no usagesprivate function validateTokenIfNeeded(Client Sclient, PaginationState $state): voidi...}private function executeSearchRequest(Client Sclient, string $endpoint, array $payload, PaginationStatryfSresponse = Sclient->qetInstance(->qetClient(->request( method: "POST', Sendpoint, ['ison' =Sheaders = sresponse->qetheaderso^Sthis->logger->info('[Hubspot] DEBUG Getting headers'. ['headers' => Sheaders ?? [.return Sresponse->toArrav@:} catch (\Exception $e) {if Sclient->isUnauthorizedExceot.ionSe))<Sthis->Logger->warning('[Hubspot) Got 401 during pagination, attempting token refresh',*t'team id' => ScLient-›aetconfia0->getTeamo->qetido.'error' => Se->aetMessageolSclient->ensureValidTokendSstate->undatel.astTokenCheckotry{Snesnonse = Sclient->aetInstance@->aetclien+@->nequest/method: pnst: Sendnoint$this->logger->info('[Hubspot] Token refresh and retry successful'. [team id' => $client->getConfiq(->qetTeam©->getId•.= custom.log=laravel.logA SF (jiminny@localhost]4 HS_local jiminny@localhost] >iti accounts (jiminny@localhost]A console (PROD)# console [eu)A console [STAGING]Tx: Autov liminnySELECT * FRUM crm_tieLd_data WhERE crm_layout_entity_1d IN (6494,6495,64%6,6497,0498,0499); m 06 A1 x6 ~37 VISELECTGEEEc0 coco coCONCAT(u.id, CASE WHEN .id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idu.emarsa.*t.owner 1d FROM social accounts saJOIN users uon v.id = sa.sociable_idJOIN teams t ..n<->1: on t.id = u.team_idWHERE U.team_id = 2 and sa.provider = 'hubspot':select * from social accounts whenp id = 1499.select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:select * trom crm tleld data where obnect 10 = 15057SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):select * rrom reatuselect * from team features where team id IN (1):select * from team features where feature id IN (36):SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm_providersid = '111751';"Ssiud & "HURSPOT WERHOOK SYNCIS# $feature = Feature::query(->where('slug', $slug)-›firstO;# TeanFeature::quenv()->create(['feature id' => Sfeature->aetId() Iteam id! => Steam->aetId@l)•# hubsnot wehhook metricoselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•select * from activities where crm confiquration id = 2:select settings from crm confiqurations where id = 11;Lukas sterka 121 • nov100% 1P• Inu / May 1/.30•30AskJiminnyReportActivityServiceTest vCascadeHubspot Rate LimitingHubSpot Search Rate+0 ..168a Fast Context Find all HubSpot search APl calls includina crm/vs/obiects//search endpoints and request methods ... In 3.28SBased on my analvsis, here are all HubSpot search APl calls in the codebase:Summary of All HubSpot Search API Calls1. Pacination Service (HubsootPaqinat ionService.oho line 36)• php$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";Used by opportunity sync strategies to paginate through deals/companies/contacts2. Service.php - Multiple search endpointsMethodEndpointPurpose/crm/v3/obiects/companies.contacts)/searchName search for contacts/companieshandLePhoneSearchRequestol/crm/v3/obiects/contacts/searchPhone number matchingcrm/v3/obrects/calls/searchImport calls bv periodsearchcaulsForPeriodByPage)/crm/v3/objects/calls/searchPaginated call search/crm/v3/objects/calls/searchFind call by recording URL3. PayloadBuilder.php generates payloads for•generatedoportunitySearchPavioad - Deal search tor sunc stratediesgenerateSearchContactsByNamePayload() - Contact name searchgeneratePhoneSearchPav oad - Phone search• generateSearchCallsByPeriodPayload() - Call period searchgeneratecetcallsPavloado - Call import searchaenerateSearchCal1BvTokenPavload/ - Calll bv token searchlKey Findings1. No RetrvzAfter header in cearch recnonses - Your dehua lod confirms this. HubSnot doecn't send rate limit quidance in search2 Pate limits are enforced cerver-cide.ten cecondlv rollina • 10 reauects ner 10 ceconde•secondly → 1 request per second (for search endpoints)3. Current retry loaic (in Service, oho lines 1723-1727)AohrSretrvAfter = isset (Sresponse ('policvName'))fc. Cresnonsel"nolicvNane'l == celf.-TEN CECONDIY pOLITNG pOLTCY? self::TEN_SECONDLY ROLLING LIMITII10 seconds•1. I Default 1 cecondRecommendationSince soarch ADic don!t return Petru-After headers the current annroach of ucing nolicvlane from the error recnonce body ic thelets move all search request to header undelfo 4 spaces...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4907
|
|
4906
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
Lukas Kovalik
5:38
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Reducing noise
Firefox FileEdit→ CViewHistoryBookmarksProfilesToolsWindowHelp= [ meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% <478 • Thu 7 May 17:38:01Stefka StoyanovaLukas Kovalik5:38 PM | Lukas/Stefka 121• 7:23.Sộ3...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4906
|
|
4905
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
Lukas Kovalik
5:37
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Reducing noise
PhostormINavicatecodeFV faVsco.js?9 master kProiectT DeleteCrmEntity lratt.onpCrmObiects© RateLimitException.php© HubspotPaginationService.php X ©PaginationState.php> • DecorateActivityDummy> • Helpers© Hubs©) Hubspotsinglesyncstrategy.phpv D HubspotAccountSyncStrateHubspotwebhookbatchsyncstrateav.ongmoortopportunity8atch.ono>@ ActionsImportBatchJobTrait.pho(c) Hubspot/Service.php() PayloadBuilder.phpC) Companies.php© MatchActivityCrmData.php_ contactsyncstrateC) CrmActivityService.pho(C) CachedCrmServiceDecorator.php•Hubspot/....SyncCrmEntitiesTrait.phpC) Pipedrive/Service.php•DDIe1Servicelnterface.oho@ OpportunitvSvncTest.ohn• U rielas• W Journa.cllass HuhsnotPaoinationServilceA19 V1 лMetadataOpponunilvsyncsuu Concerns(C) HubspotlastMoi 133(C) HubspotlastMoi 134© HubspotLastMor 139(C) HuospotlastMo(C) —uosootLastMot 134(C) Huosootsindlest 154C. HubsnotSvncStr(C) HubsnotWebhoc 155)v Padination(c) PadinationConfit 143)(c) DadinationState 14/m ProcnectSoarchStr: 14g• m Pedic• D ServiceTraitsT. OpportunitySync 168T SyncCrmEntities 169T SyncFieldsTrait.| 17€T WriteCrmTrait.pl 171> O Utils|> 0 Webhook|c) BatchSyncCollectol 174C) BatchSyncRedisSer 179c) Client.phpC) ClosedDealStagess 177CDealFieldsService.c 178C) DecorateActiviv.or 176c)FieldDerinitions.onr 180C) FieldivoeConvertel 18.1) HubsootClientinterl 182C) HubsootTokenMan: 183© PayloadBuilder.php 184C) RemoteCrmObiecti 1899 ResnonseNormalize 194(c) Service nhn(C) SvncFioldAction nh 199(C) SvncRelatedActiviti 190private function handlePaginationStrategy(return Soavload:lusageprivate function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool{.:no usagesprivate function validateTokenIfNeeded(Client Sclient, PaginationState $state): voidi...}private function executeSearchRequest(Client Sclient, string $endpoint, array $payload, PaginationStatryfSresponse = Sclient->qetInstance(->qetClient(->request( method: "POST', Sendpoint, ['ison' =Sheaders = sresponse->qetheaderso^Sthis->logger->info('[Hubspot] DEBUG Getting headers'. ['headers' => Sheaders ?? [.return Sresponse->toArrav@:} catch (\Exception $e) {if Sclient->isUnauthorizedExceot.ionSe))<Sthis->Logger->warning('[Hubspot) Got 401 during pagination, attempting token refresh',*t'team id' => ScLient-›aetconfia0->getTeamo->qetido.'error' => Se->aetMessageolSclient->ensureValidTokendSstate->undatel.astTokenCheckotry{Snesnonse = Sclient->aetInstance@->aetclien+@->nequest/method: pnst: Sendnoint$this->logger->info('[Hubspot] Token refresh and retry successful'. [team id' => $client->getConfiq(->qetTeam©->getId•.= custom.log=laravel.logA SF (jiminny@localhost]4 HS_local jiminny@localhost] >iti accounts (jiminny@localhost]A console (PROD)# console [eu)A console [STAGING]Tx: Autov liminnySELECT * FRUM crm_tieLd_data WhERE crm_layout_entity_1d IN (6494,6495,64%6,6497,0498,0499); m 06 A1 x6 ~37 VISELECTGEEEc0 coco coCONCAT(u.id, CASE WHEN .id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idu.emarsa.*t.owner 1d FROM social accounts saJOIN users uon v.id = sa.sociable_idJOIN teams t ..n<->1: on t.id = u.team_idWHERE U.team_id = 2 and sa.provider = 'hubspot':select * from social accounts whenp id = 1499.select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:select * trom crm tleld data where obnect 10 = 15057SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):select * rrom reatuselect * from team features where team id IN (1):select * from team features where feature id IN (36):SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm_providersid = '111751';"Ssiud & "HURSPOT WERHOOK SYNCIS# $feature = Feature::query(->where('slug', $slug)-›firstO;# TeanFeature::quenv()->create(['feature id' => Sfeature->aetId() Iteam id! => Steam->aetId@l)•# hubsnot wehhook metricoselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•select * from activities where crm confiquration id = 2:select settings from crm confiqurations where id = 11;Lukas sterka 121 • nov100% 1P• Inu / May 1/.3/:04AskJiminnyReportActivityServiceTest vCascadeHubspot Rate LimitingHubSpot Search Rate+0 ..168a Fast Context Find all HubSpot search APl calls includina crm/vs/obiects//search endpoints and request methods ... In 3.28SBased on my analvsis, here are all HubSpot search APl calls in the codebase:Summary of All HubSpot Search API Calls1. Pacination Service (HubsootPaqinat ionService.oho line 36)• php$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";Used by opportunity sync strategies to paginate through deals/companies/contacts2. Service.php - Multiple search endpointsMethodEndpointPurpose/crm/v3/obiects/companies.contacts)/searchName search for contacts/companieshandLePhoneSearchRequestol/crm/v3/obiects/contacts/searchPhone number matchingcrm/v3/obrects/calls/searchImport calls bv periodsearchcaulsForPeriodByPage)/crm/v3/objects/calls/searchPaginated call search/crm/v3/objects/calls/searchFind call by recording URL3. PayloadBuilder.php generates payloads for•generatedoportunitySearchPavioad - Deal search tor sunc stratediesgenerateSearchContactsByNamePayload() - Contact name searchgeneratePhoneSearchPav oad - Phone searchgenerateSearchCallsByPeriodPayload() - Call period searchgeneratecetcallsPavloado - Call import searchaenerateSearchCal1BvTokenPavload/ - Calll bv token searchlKey Findings1. No RetrvzAfter header in cearch recnonses - Your dehua lod confirms this. HubSnot doecn't send rate limit quidance in search2 Pate limits are enforced cerver-cide.ten cecondlv rollina • 10 reauects ner 10 ceconde•secondly → 1 request per second (for search endpoints)3. Current retry loaic (in Service, oho lines 1723-1727)AohrSretrvAfter = isset (Sresponse ('policvName'))fc. Cresnonsel"nolicvNane'l == celf.-TEN CECONDIY pOLITNG pOLTCY? self::TEN_SECONDLY ROLLING LIMITII10 seconds•1. I Default 1 cecondRecommendationSince soarch ADic don!t return Petru-After headers the current annroach of ucing nolicvlane from the error recnonce body ic thelets move all search request to header undelfo 4 spaces...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4905
|
|
4904
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
Lukas Kovalik
5:37
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Reducing noise
Firefox FileEdit→ CViewHistoryBookmarksProfilesToolsWindowHelp= [ meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 - now100% <478 • Thu 7 May 17:37:42=Stefka StoyanovaLukas Kovalik5:37 PM | Lukas/Stefka 121• 7:04Lộ3...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4904
|
|
4903
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
Lukas Kovalik
5:37
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Reducing noise
PhostormINavicatecodeFV faVsco.js?9 master kProiectT DeleteCrmEntity lratt.onpCrmObiects© RateLimitException.php© HubspotPaginationService.php X ©PaginationState.php> • DecorateActivityDummy> • Helpers© Hubs©) Hubspotsinglesyncstrategy.phpv D HubspotAccountSyncStrateHubspotwebhookbatchsyncstrateav.ongmoortopportunity8atch.ono>@ ActionsImportBatchJobTrait.pho(c) Hubspot/Service.php() PayloadBuilder.phpC) Companies.php© MatchActivityCrmData.php_ contactsyncstrateC) CrmActivityService.pho(C) CachedCrmServiceDecorator.php•Hubspot/....SyncCrmEntitiesTrait.phpC) Pipedrive/Service.php•DDIe1Servicelnterface.oho@ OpportunitvSvncTest.ohn• U rielas• W Journa.cllass HuhsnotPaoinationServilceA19 V1 лMetadataOpponunilvsyncsuu Concerns(C) HubspotlastMoi 133(C) HubspotlastMoi 134© HubspotLastMor 139(C) HuospotlastMo(C) —uosootLastMot 134(C) Huosootsindlest 154C. HubsnotSvncStr(C) HubsnotWebhoc 155)v Padination(c) PadinationConfit 143)(c) DadinationState 14/m ProcnectSoarchStr: 14g• m Pedic• D ServiceTraitsT. OpportunitySync 168T SyncCrmEntities 169T SyncFieldsTrait.| 17€T WriteCrmTrait.pl 171> O Utils|> 0 Webhook|c) BatchSyncCollectol 174C) BatchSyncRedisSer 179c) Client.phpC) ClosedDealStagess 177CDealFieldsService.c 178C) DecorateActiviv.or 176c)FieldDerinitions.onr 180C) FieldivoeConvertel 18.1) HubsootClientinterl 182C) HubsootTokenMan: 183© PayloadBuilder.php 184C) RemoteCrmObiecti 1899 ResnonseNormalize 194(c) Service nhn(C) SvncFioldAction nh 199(C) SvncRelatedActiviti 190private function handlePaginationStrategy(return Soavload:lusageprivate function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool{.:no usagesprivate function validateTokenIfNeeded(Client Sclient, PaginationState $state): voidi...}private function executeSearchRequest(Client Sclient, string $endpoint, array $payload, PaginationStatryfSresponse = Sclient->qetInstance(->qetClient(->request( method: "POST', Sendpoint, ['ison' =Sheaders = sresponse->qetheaderso^Sthis->logger->info('[Hubspot] DEBUG Getting headers'. ['headers' => Sheaders ?? [.return Sresponse->toArrav@:} catch (\Exception $e) {if Sclient->isUnauthorizedExceot.ionSe))<Sthis->Logger->warning('[Hubspot) Got 401 during pagination, attempting token refresh',*t'team id' => ScLient-›aetconfia0->getTeamo->qetido.'error' => Se->aetMessageolSclient->ensureValidTokendSstate->undatel.astTokenCheckotry{Snesnonse = Sclient->aetInstance@->aetclien+@->nequest/method: pnst: Sendnoint$this->logger->info('[Hubspot] Token refresh and retry successful'. [team id' => $client->getConfiq(->qetTeam©->getId•.= custom.log=laravel.logA SF (jiminny@localhost]4 HS_local jiminny@localhost] >iti accounts (jiminny@localhost]A console (PROD)# console [eu)A console [STAGING]Tx: Autov liminnySELECT * FRUM crm_tieLd_data WhERE crm_layout_entity_1d IN (6494,6495,64%6,6497,0498,0499); m 06 A1 x6 ~37 VISELECTGEEEc0 coco coCONCAT(u.id, CASE WHEN .id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idu.emarsa.*t.owner 1d FROM social accounts saJOIN users uon v.id = sa.sociable_idJOIN teams t ..n<->1: on t.id = u.team_idWHERE U.team_id = 2 and sa.provider = 'hubspot':select * from social accounts whenp id = 1499.select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:select * trom crm tleld data where obnect 10 = 15057SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):select * rrom reatuselect * from team features where team id IN (1):select * from team features where feature id IN (36):SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm_providersid = '111751';"Ssiud & "HURSPOT WERHOOK SYNCIS# $feature = Feature::query(->where('slug', $slug)-›firstO;# TeanFeature::quenv()->create(['feature id' => Sfeature->aetId() Iteam id! => Steam->aetId@l)•# hubsnot wehhook metricoselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•select * from activities where crm confiquration id = 2:select settings from crm confiqurations where id = 11;Lukas sterka 121 • nov100% 1P• Inu / May 1/•3/•21AskJiminnyReportActivityServiceTest vCascadeHubspot Rate LimitingHubSpot Search Rate+0 ..168a Fast Context Find all HubSpot search APl calls includina crm/vs/obiects//search endpoints and request methods ... In 3.28SBased on my analvsis, here are all HubSpot search APl calls in the codebase:Summary of All HubSpot Search API Calls1. Pacination Service (HubsootPaqinat ionService.oho line 36)• php$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";Used by opportunity sync strategies to paginate through deals/companies/contacts2. Service.php - Multiple search endpointsMethodEndpointPurpose/crm/v3/obiects/companies.contacts)/searchName search for contacts/companieshandLePhoneSearchRequestol/crm/v3/obiects/contacts/searchPhone number matchingcrm/v3/obrects/calls/searchImport calls bv periodsearchcaulsForPeriodByPage)/crm/v3/objects/calls/searchPaginated call search/crm/v3/objects/calls/searchFind call by recording URL3. PayloadBuilder.php generates payloads for•generatedoportunitySearchPavioad - Deal search tor sunc stratediesgenerateSearchContactsByNamePayload() - Contact name searchgeneratePhoneSearchPav oad - Phone searchgenerateSearchCallsByPeriodPayload() - Call period searchgeneratecetcallsPavloado - Call import searchaenerateSearchCal1BvTokenPavload/ - Calll bv token searchlKey Findings1. No RetrvzAfter header in cearch recnonses - Your dehua lod confirms this. HubSnot doecn't send rate limit quidance in search2 Pate limits are enforced cerver-cide.ten cecondlv rollina • 10 reauects ner 10 ceconde•secondly → 1 request per second (for search endpoints)3. Current retry loaic (in Service, oho lines 1723-1727)AohrSretrvAfter = isset (Sresponse ('policvName'))fc. Cresnonsel"nolicvNane'l == celf.-TEN CECONDIY pOLITNG pOLTCY? self::TEN_SECONDLY ROLLING LIMITII10 seconds•1. I Default 1 cecondRecommendationSince soarch ADic don!t return Petru-After headers the current annroach of ucing nolicvlane from the error recnonce body ic thelets move all search request to header undelfo 4 spaces...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4903
|
|
4902
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
Lukas Kovalik
5:37
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Reducing noise
Firefox FileEdit→ CViewHistoryBookmarksProfilesToolsWindowHelp(ah|= [ meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 - now100% <478 • Thu 7 May 17:37:09Stefka StoyanovaLukas KovalikМA ПAРTТ5:37 PM | Lukas/Stefka 121• 6:31Sộ3...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4902
|
|
4901
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
Lukas Kovalik
5:37
PM
Lukas/Stefka 121
Lukas/Stefka 121
Reducing noise
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Reducing noise...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4901
|
|
4900
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
Lukas Kovalik
5:37
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Firefox FileEditView→ CHistoryBookmarksProfilesToolsWindowHelp(alol= [ meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% <478 • Thu 7 May 17:37:00+Stefka StoyanovaLukas KovalikAtm5:37 PM | Lukas/Stefka 121• 6:22Sộ3...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4900
|
|
4899
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
FirefoxFileEdit→ CViewHistoryBookmarksProfilesToolsWindowHelp(ah|= [ meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% <78 • Thu 7 May 17:36:58=Pop out this videoStefka StoyanovaLukas Kovalik5:36 PM | Lukas/Stefka 121• 6:20|Sộ3...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4899
|
|
4898
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
PhostormINavicatecodeFV faVsco.js?9 master kProiectT DeleteCrmEntity lratt.onpCrmObiects© RateLimitException.php© HubspotPaginationService.php X ©PaginationState.php> • DecorateActivityDummy> • Helpers© Hubs©) Hubspotsinglesyncstrategy.phpv D HubspotAccountSyncStrateHubspotwebhookbatchsyncstrateav.ongmoortopportunity8atch.ono>@ ActionsImportBatchJobTrait.pho(c) Hubspot/Service.php() PayloadBuilder.phpC) Companies.php© MatchActivityCrmData.php_ contactsyncstrateC) CrmActivityService.pho(C) CachedCrmServiceDecorator.php•Hubspot/....SyncCrmEntitiesTrait.php(C) Pipedrive/Service.php•DDIe1Servicelnterface.oho@ OpportunitvSvncTest.ohp• U rielas• W Journa.cllass HuhsnotPaoinationServilceA19 V1 лMetadataOpponunilvsyncsuu Concerns(C) HubspotlastMoi 133(C) HubspotlastMoi 134© HubspotLastMor 139(C) HuospotlastMo(C) —osootLastMot 134(C) Huosootsindlest 154C. HubsnotSvncStr(C) HubsnotWebhoc 155)v Padination(c) PadinationConfit 143)(c) DadinationState 14/m ProcnectSoarchStr: 14g• m Pedic• D ServiceTraitsn. OpportunitySync 168T SyncCrmEntities 169T SyncFieldsTrait.| 176T WriteCrmTrait.pl 171> O Utils|> 0 Webhook|c) BatchSyncCollectol 174C) BatchSyncRedisSer 179c) Client.phpC) ClosedDealStagess 177CDealFieldsService.c 178C) DecorateActiviv.or 176c)FieldDerinitions.onr 180C) FieldivoeConvertel 18.1) Hubsootclientinterl 182C) HubsootTokenMan: 183© PayloadBuilder.php 184C) RemoteCrmObiecti 1899 ResnonseNormalize 194(c) Service nhn(C) SvncFioldAction nh 199(C) SvncRelatedActiviti 190private function handlePaginationStrategyCreturn Soavload:lusageprivate function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool{.:no usagesprivate function validateTokenIfNeeded(Client Sclient, PaginationState $state): voidi...}private function executeSearchRequest(Client Sclient, string $endpoint, array $payload, PaginationStatryfSresponse = Sclient->qetInstance(->qetClient(->request( method: "POST', Sendpoint, ['ison' =Sheaders = sresponse->qetheaderso^Sthis->logger->info('[Hubspot] DEBUG Getting headers'. ['headers' => Sheaders ?? [.return Sresponse->toArrav@:} catch (\Exception $e) {if Sclient->isUnauthorizedExceot.ionSe))<Sthis->Logger->warning('[Hubspot) Got 401 during pagination, attempting token refresh',*t'team id' => ScLient-›aetconfia0->getTeamo->qetido.'error' => Se->aetMessageolSclient->ensureValidTokendSstate->undatel.astTokenCheckotry{Snesnonse = Sclient->aetInstance@->aetclien+@->nequest/method: pnst: Sendnoint$this->logger->info('[Hubspot) Token refresh and retry successful', [team id' => $client->getConfiq(->qetTeam©->getId•.= custom.log=laravel.logA SF (jiminny@localhost]4 HS_local jiminny@localhost] >iti accounts (jiminny@localhost]A console (PROD]# console [eu)A console [STAGING]Tx: Autov liminnySELECT * FRUM crm_tieLd_data WhERE crm_layout_entity_1d IN (6494,6495,64%6,6497,0498,0499); m 06 A1 x6 ~37 VISELECTCONCAT(u.id, CASE WHEN .id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,u.emarsa.*t.owner 1d FROM social accounts saJOIN users uon v.id = sa.sociable_idJOIN teams t ..n<->1: on t.id = u.team_idWHERE u.team_id = 2 and sa.provider = 'hubspot':select * from social accounts whene id = 1409•select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:select * trom crm tleld data where obnect 10 = 15057SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):select * rrom reatuselect * from team features where team id IN (1):select * from team features where feature id IN (36):SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm provider 1d = 111751'4"Ssiud & "HURSPOT WERHOOK SYNCI# Steam = Jiminnv Models Team: • find(2)•# $feature = Feature::query(->where('slug', $slug)->firstO:# TeanFeature::quenv()->create(['feature id' => Sfeature->aetId() Iteam id! => Steam->aetId@l)•# hubsnot wehhook metricslselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•c0 coselect * from activities where crm confiquration id = 2:select settings from crm confiqurations where id = 11;Lukas sterka 121 • nov100% 1P• Inu / May 1/•30.02AskJiminnyReportActivityServiceTest vCascadeHubspot Rate LimitingHubSpot Search Rate+0 ..168a Fast Context Find all HubSpot search APl calls includina crm/vs/obiects//search endpoints and request methods ... In 3.28SBased on my analvsis, here are all HubSpot search APl calls in the codebase:Summary of All HubSpot Search API Calls1. Pacination Service (HubsootPaqinat ionService.oho line 36)• php$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";Used by opportunity sync strategies to paginate through deals/companies/contacts2. Service.php - Multiple search endpointsMethodEndpointPurpose/crm/v3/obiects/companies.contacts)/searchName search for contacts/companieshandLePhoneSearchRequestol/crm/v3/obiects/contacts/searchPhone number matchingcrm/v3/obrects/calls/searchImport calls by periodsearchcaulsForPeriodByPage)/crm/v3/objects/calls/searchPaginated call search/crm/v3/objects/calls/searchFind call by recording URL3. PayloadBuilder.php generates payloads for•generatedoportunitySearchPavioad - Deal search tor sunc stratediesgenerateSearchContactsByNamePayload() - Contact name searchgeneratePhoneSearchPav oad - Phone searchgenerateSearchCallsByPeriodPayload() - Call period searchgeneratecetcallsPavloado - Call import searchaenerateSearchCal1BvTokenPavload/ - Calll bv token searchlKey Findings1. No RetrvzAfter header in cearch recnonses - Your dehua lod confirms this. HubSnot doecn't send rate limit quidance in search2 Pate limits are enforced cerver-cide.ten cecondlv rollina • 10 reauects ner 10 ceconde•secondly → 1 request per second (for search endpoints)3. Current retry loaic (in Service, oho lines 1723-1727)AohrSretrvAfter = isset (Sresponse ('policvName'))fc. Cresnonsel"nolicvNane'l == celf.-TEN CECONDIY pOLITNG pOLTCY? self::TEN_SECONDLY ROLLING LIMITII10 seconds•1. I Default 1 cecondRecommendationSince soarch ADic don!t return Petru-After headers the current annroach of ucing nolicvlane from the error recnonce body ic thelets move all search request to header undelío 4 spaces...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4898
|
|
4897
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
FirefoxFile Edit View→ CHistoryBookmarksProfilesToolsWindowHelp‹>0(ah|= [ meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 • now100% <78• Thu 7 May 17:36:54Ask GeminiGemini is available to answer questionsabout meeting discussions. It won't create arecording or store caption data after themeeting ends. The meeting host can turn itoff in settings.Learn moreDon't show againStefka StoyanovaLukas Kovalik5:36 PM | Lukas/Stefka 121• 6:16Sộ3...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4897
|
|
4896
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn on camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Close
New
Ask Gemini
Ask Gemini
Gemini is available to answer questions about meeting discussions. It won’t create a recording or store caption data after the meeting ends. The meeting host can turn it off in settings.
Learn more about Gemini
Don't show again...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4896
|
|
4895
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn on camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Close
New
Ask Gemini
Ask Gemini
Gemini is available to answer questions about meeting discussions. It won’t create a recording or store caption data after the meeting ends. The meeting host can turn it off in settings.
Learn more about Gemini
Don't show again...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4895
|
|
4894
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn off microphone
Video settings
Turn on camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Close
New
Ask Gemini
Ask Gemini
Gemini is available to answer questions about meeting discussions. It won’t create a recording or store caption data after the meeting ends. The meeting host can turn it off in settings.
Learn more about Gemini
Don't show again
Your microphone is on....
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4894
|
|
4893
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn on microphone
Video settings
Turn on camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Close
New
Ask Gemini
Ask Gemini
Gemini is available to answer questions about meeting discussions. It won’t create a recording or store caption data after the meeting ends. The meeting host can turn it off in settings.
Learn more about Gemini
Don't show again...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4893
|
|
4892
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn on microphone
Video settings
Turn on camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Firefox File EditView→ CHistoryBookmarksProfilesToolsWindowHelp(ah|• = @ @ meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 • now100% <478• Thu 7 May 17:36:50New Ask GeminiGemini is available to answer questionsabout meeting discussions. It won't create arecording or store caption data after themeeting ends. The meeting host can turn itoff in settings.Learn moreDon't show againStefka StoyanovaLukas Kovalik5:36 PM | Lukas/Stefka 121• 6:12.Sộ3...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4892
|
|
4891
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn on microphone
Video settings
Turn on camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Close
New
Ask Gemini
Ask Gemini
Gemini is available to answer questions about meeting discussions. It won’t create a recording or store caption data after the meeting ends. The meeting host can turn it off in settings.
Learn more about Gemini
Don't show again
Stefka Stoyanova joined
Background is no longer replaced...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4891
|
|
4890
|
Meet - Lukas/Stefka 121
Close tab
New Tab
Open Goo Meet - Lukas/Stefka 121
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
2
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Stefka Stoyanova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:36
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn on microphone
Video settings
Turn on camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Close
New
Ask Gemini
Ask Gemini
Gemini is available to answer questions about meeting discussions. It won’t create a recording or store caption data after the meeting ends. The meeting host can turn it off in settings.
Learn more about Gemini
Don't show again
Stefka Stoyanova joined
Background is no longer replaced...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
4890
|
|
4889
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$headers = $response->getHeaders();
$this->logger->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
4889
|
|
4888
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
https://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624
https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0
SELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;
# 609126 softphone tr. 11241
SELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;
# 608874 conference tr. 11226 crmId: 103422236596
select * from ai_prompts where transcription_id IN (11241, 11226);
select * from activity_summary_logs where activity_id = 608874;
select * from sidekick_settings;
select * from default_activity_types;
select * from crm_field_data where activity_id = 1223;
select * from crm_layouts where crm_configuration_id = 2;
SELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);
select * from crm_fields where crm_configuration_id = 11 and object_type = 'event';
SELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);
SELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;
SELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u
on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 2 and sa.provider = 'hubspot';
select * from social_accounts where id = 1499;
select * from opportunities where team_id = 2
and crm_provider_id IN ('51317301383');
select * from contacts where id = 85;
select * from opportunities where team_id = 2 order by id desc;
select * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112
select * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112
select * from opportunity_contacts where opportunity_id = 5117;
select * from crm_field_data where object_id = 1365;
SELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);
select * from features;
select * from team_features where team_id IN (1);
select * from team_features where feature_id IN (36);
SHOW CREATE TABLE opportunity_contacts;
SELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';
# $slug = 'HUBSPOT_WEBHOOK_SYNC';
# $team = Jiminny\Models\Team::find(2);
# $feature = Feature::query()->where('slug', $slug)->first();
# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);
# hubspot_webhook_metrics
select * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365
SELECT * FROM opportunity_contacts WHERE opportunity_id = '414';
SELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';
select * from contacts where id in (414, 464);
select * from activities where crm_configuration_id = 2;
select settings from crm_configurations where id = 11;
select * from teams; # 1, 2
select * from users;
select * from crm_configurations where id = 39;
select * from team_features where team_id = 2;
select * from features;
# SELECT * FROM opportunities WHERE crm_configuration_id = 2
# order by id desc;
# and crm_provider_id = '49908861993';
select * from activity_providers where id IN (443, 202, 203, 227);
select * from activity_imports where id = 795889;
select c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id
where c.provider = 'hubspot';
select * from crm_configurations crm JOIN teams t on crm.team_id = t.id
where provider = 'hubspot';
SELECT * FROM teams WHERE id = 31;
SELECT * FROM users WHERE id = 257;
SELECT * FROM opportunities WHERE team_id = 2;
select * from opportunity_contacts where opportunity_id = 5124;
select * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)
select * from activities where crm_configuration_id = 13;
SELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141
select id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;
SELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;
SELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;
SELECT * FROM contacts WHERE team_id = 2 order by id desc;
select * from opportunity_contacts where contact_id = 6223;
SELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;
select * from crm_profiles where crm_configuration_id = 2;
select * from activities where account_id = 46;
Sync Changes
Hide This Notification
Code changed:
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
4888
|
|
4887
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stoyan Tanev
Stefka Stoyanova
Ves
Galya Dimitrova
Aneliya Angelova
Vasil Vasilev
James Graham
Nikolay Ivanov
Lukas Kovalik
you
Jira Cloud
Toast
Google Calendar
Messages
Messages
Files
Files
Bookmarks
Bookmarks
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
CircleCI
APP
Today at 3:20:52 PM
3:20 PM
Deployment Successful! tada emoji
Deployment Successful!
Project
: app
When
: 05/07/2026 12:20:52
Tag
:
View Job
View Job
CircleCI
APP
Today at 4:32:50 PM
4:32 PM
New commits deployed to Prophet Prod-US:
[74673da](
https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10
https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10
) - [JY-205680](
https://jiminny.atlassian.net/browse/JY-205680
https://jiminny.atlassian.net/browse/JY-205680
): Relax action items assignee (#502) (steliyan-g)
Today at 4:33:51 PM
4:33
New commits deployed to Prophet Prod-EU:
[74673da](
https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10
https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10
) - [JY-205680](
https://jiminny.atlassian.net/browse/JY-205680
https://jiminny.atlassian.net/browse/JY-205680
): Relax action items assignee (#502) (steliyan-g)
GitHub
APP
Today at 5:03:19 PM
5:03 PM
3 new commits
3 new commits
pushed to
master
master
by
mihailmihaylovjiminny
mihailmihaylovjiminny
beb8e387
beb8e387
- JY-20817: Fix deleting old tracks
8f177131
8f177131
- Merge branch 'master' into JY-20817-fix-deleting-old-tracks
[CREDIT_CARD]
- Merge pull request #12052 from jiminny/JY-20817-fix-deleting-old-tracks
jiminny/app
jiminny/app
|
Added by
GitHub
GitHub
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
CircleCI
APP
Today at 5:29:41 PM
5:29 PM
Deployment Successful! tada emoji
Deployment Successful!
Project
: app
When
: 05/07/2026 14:29:40
Tag
:
View Job
View Job
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Channel releases
ActivityMoreSlackcalVIewMistonWindowHelpJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages3 Aneliya Angelova, ...2. Stoyan Tanev •a. Stefka Stoyanova8. Ves%. Galya DimitrovaF. Aneliya Angelova€. Vasil Vasilev&. James GrahamC.. Nikolay Ivanov• Lukas Kovali...eadersonView Jobpot] DEBUG Getting headers', [::: ADoSр? [1,Jira CloudMessage #releases® Toast+ AalGoogle Cale...#releases8 22Messagese Files• Bookmarks(https://githu.lodaynv/prophet/comm1746/36258939901011621 /50006520304b3dce10) - [JY-205680](https://jiminny.atlassian.net/browse/JY-205680): Relax action items assignee(#502) (steliyan-g)СITНITD APP 5:03 PM3 new commits pushed to master bymihailmihaylov iminnybeb8e387 - Y-20817: Fix deleting oldtracks8f177131) - Merge branch 'master' into1Y-20817-hx-delehng-old-tracks(12295204) - Merge pull request #12052from jiminny/JY-20817-fix-deleting-old-tracksjuminny/app Added by GitHubCircled App 5-20 pMlO Deployment Successful! 3Prolect.When:05/07/2026 14:9:401ObjectDetach.phpeexception.ong© DeleteCrmEntityTrait.php© HubspotPaginationService.php x © PaginationState.php© BadRequest.phpT OpportunitySyncTrait.php©) Hubspotsinglesyncstrategy.php© Importopportunity8atch.ono•PayloadBuilder.phpC) Companies.php© MatchActivityCrmData.phpbr.php•Hubspot/...SyncCrmEntitiesTrait.phpC) Pipedrive/Service.phpA19 V1 лcrategyCsetPagination(PaginationState $state, int SresultsPerPage): boolf5aded(Client $client, PaginationState $state): voidf...}ast(Client $client, string $endpoint, array $payload, PaginationStastance()->getClient()->request ( method: 'POST', $endpoint, ['json' =redExcentionSe))<Sthis->Logger->warning('[Hubspot) Got 401 during pagination, attempting token refresh',tC) ClosedDealStagess 177'team id' => ScLient-›aetconfia0->qetTeamo->getido.@ DealFieldsService.p 178'error" => Se->aetMessageoC) DecorateActiviv.or 176c)FieldDerinitions.onr 180C) FieldivoeConvertel 18.• HubspotClientintert 182$client->ensureVaLidToken();Sstate-›updateLast|rokenCheck():C) HubsootTokenMan: 183C) PavloadBuilder.oho 184try {Sresponse = $client->getInstance->getClient(->request(method:"pnst: Sendnoint* ResponseNormalize 186(c) Service nhn(C) SvncFioldAction nh 199(C) SvncRelatedActiviti 190$this->logger->info('[Hubspot] Token refresh and retry successful', ['team id' => Sclient-›getConfig()->getTeam()-›getIdO).arQube for INE suaastions: Detect more security issues in your PHP files // Try SonarQube Cloud for free // Download SonarQube Server // Learn more // Don't ask again (today 10:25)E custom.logA console (PROD]E laravel.log# console [eu)Tx: Auto vA SF [jiminny@localhost]A console [STAGING]av owne4 HS_Jocal [iminny@localhost] Xti accounts (jiminny@localhost]dajiminny ~SELECT * FRUM crm_tieLd_data WhERE crm_layout_entity_1d IN (6494,6495,64%6,6497,0498,0499); m | 06 A1 x6 ^37 VISELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,u.emarsa.*,t.owner 1d FROM social accounts saJOIN users uon v.id = sa.sociable_idJOIN teams t 1..n<->1: on t.id = u.team_idWHERE U.team_id = 2 and sa.provider = 'hubspot Fselect * from social_accounts where id = 1499;select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team_id = 2 order by id desc;select * from opportunities where team_id = 2 and crm_provider_id = ^51317301383'; # 5112select * from opportunities where team_id = 2 and crm_provider_id = ^55976759904'; # 5112select * from opportunity_contacts where opportunity_id = 5117;select * trom crm tield data where obnect 10 = 1505*SELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);select * rrom reatuselect * from team features where team id IN (1):select * from team_features where feature_id IN (36);SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm provider 1d = 111751'4"Ssiud & "HURSPOT WERHOOK SYNCIOAEATc0 co# $feature = Feature::query(->where('slug', $slug)->firstO:# TeamFeature: :query() -›create(['feature_id' => Sfeature-›getId(), 'team_id' => Steam-›getId()1);# hubspot_webhook_metricsselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity_contacts WHERE opportunity_id = '414';SELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';colort + fnom contante whono id in (414464):select * from activities where crm_configuration_id = 2;select settings from crm_configurations where id = 11;Lukas sterka 121 • now100% 52Thu 7 May 17:36:45L AskJiminnyReportActivityServiceTest ~CascadeHubspot Rate LimitingHubSpot Search Rate+0 ..168a Fast Context Find all HubSpot search APl calls includina crm/vs/obiects//search endpoints and request methods ... In 3.28SBased on my analvsis, here are all HubSpot search APl calls in the codebase:Summary of All HubSpot Search API Calls1. Pagination Service (HubspotPaginationService.php line 36)• phpsendpoint = Client::BASE_URL . "/crm/v3/objects/{Stype}/search";Used by opportunity sync strategies to paginate through deals/companies/contacts2. Service.php - Multiple search endpointsMethodEndpointPurpose/crm/v3/obiects/companies.contacts)/searchName search for contacts/companieshandLePhoneSearchRequestol/crm/v3/obiects/contacts/searchPhone number matchingcrm/v3/obrects/calls/searchImport calls by periodsearchcaulsForPeriodByPage)/crm/v3/objects/calls/search/Crm/v3/objects/calls/searchPaginated call searchFind call by recording URL3. PayloadBuilder.php generates payloads for•generatedoportunitySearchPavioad - Deal search tor sunc stratediesgenerateSearchContactsByNamePayload() - Contact name searchgeneratePhoneSearchPav oad - Phone searchgenerateSearchCallsByPeriodPayload() - Call period searchgeneratecetcallsPavloado - Call import searchaenerateSearchCal1BvTokenPavload/ - Calll bv token searchlKey Findings1. No RetrvzAfter header in cearch recnonses - Your dehua lod confirms this. HubSnot doecn't send rate limit quidance in search2. Rate limits are enforced server-side:•ten_secondly_rolling 10 requests per 10 seconds•secondly → 1 request per second (for search endpoints)3. Current retry loaic (in Service, oho lines 1723-17277D phpSretrvAfter = isset (Sresponse ('policvName'))fc. Cresnonsel"nolicvNane'l == celf.-TEN CECONDIY pOLITNG pOLTCY? self::TEN_SECONDLY_ROLLING_LIMIT// 10 seconds•1. I Default 1 cecondRecommendationSince search APls don't return Retry-After headers, the current approach of using policyName from the error response body is thelets move all search request to header undel182•25UTF.8f 4 spaces...
|
Slack
|
releases (Channel) - Jiminny Inc - 3 new items - S releases (Channel) - Jiminny Inc - 3 new items - Slack...
|
NULL
|
4887
|
|
4886
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$headers = $response->getHeaders();
$this->logger->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
4886
|
|
4885
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$headers = $response->getHeaders();
$this->logger->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
4885
|
|
4884
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$headers = $response->getHeaders();
$this->logger->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
4884
|
|
4883
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$headers = $response->getHeaders();
$this->logger->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
4883
|
|
4882
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$headers = $response->getHeaders();
$this->logger->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
4882
|
|
4881
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
return null;
// 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) {
...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4881
|
|
4880
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
PhostormProiectcodeFV faVsco.js?9 master kLukas sterka 121 • novAskJiminnyReportActivityServiceTest -100% 2• Thu 7 May 17:35:30M CrmObiects© RateLimitException.php© PaginationState.php> • DecorateActivity>D Dummy> • Helpers© Hubsv D HubspotAccountSyncStrateHubspotwebhookbatchsyncstrateav.ongmoortopportunity8atch.ono> D ActionsImportBatchJobTrait.pho(c) Hubspot/Service.php X(C) PayloadBuilder.phoC) Companies.php© MatchActivityCrmData.php0 ContactSyncStrateC) CrmActivityService.phoCachedCrmServiceDecorator.php•Hubspot/....SyncCrmEntitiesTrait.php(C) Pipedrive/Service.phpDDIe1Servicelnterface.ohd@ OpportunitvSvncTest.ohpU rielas• C Journal10.02.22 Vasilevclass Service extends BaseService implements m48 A49 V1 X33 41AMetadata12.11.24 Kovalikprivate function findAndSyncContact(string $crmId): ?Contactv _ Opporunilvsyncsr12.11.24 Kovalik1102Concerns121124 Kovalil1192c) Hubspotlastmo12.11.24 Kovalik1104rerurn nutt(C) HubspotlastMo121104 Kovalik.1105c) Hubspotlastmo12.11.24 Kovalik1106(C) HuospotlastMo12.11.24 Kovallk110750C) -uosootLastMo(C) -uospotsindles31.05.24 NIKOlOYprivate function hasResults(array $response): boolf...}C. HubsnotSvncStr31.05.24 NKOlOY1112(C) HubsnotWebhor• M Padinationi26.02.24 Kovalik11131private function searchForPhoneNumber(string Sphone): arravd...}(C) HubsnotPadinat26.02.24 Kovalik1154(c) PadinationConfi3 usages(c) DadinationState26.02.24 Kovalik1155orivate function handlePhoneSearchRequest(string Sphone. array Spavload).M ProcnectSoarchStr.26.02.24 Kovalik1156• M PedicSendooint ='[URL_WITH_CREDENTIALS] => Sresponse9) ResnonceNormalize26.02.24 Kovalik1177C) Service nhn1178© SyncFieldAction.ph 26.02.24 Kovalik11791return Sresponse->toArravo(C) SvncRelatedActivit ne лo nл vnuaн».1180narAuhe for IdE suadestiions Detect more seamritvlisaues in vour D.Dfflles llia SonarAube Claud for free //lDownload SonarOnbe Server Illear more /llDonit ask adain /today 105251=custom.log=laravel.logA SF (jiminny@localhost]4 HS_local (iminny@localhost] >iti accounts (jiminny@localhost]A console (PROD]# console [eu)A console [STAGING]Tx: Autovav ownefsjminnySELECT * FRUM crm_tieLd_data WhERE crm_layout_entity_1d IN (6494,6495,64%6,6497,0498,0499); m | 06 A1 x6 ^CascadeHubspot Rate LimitingHubSpot Search RateSendpoint = Client:: BASE URL . "/crm/v3/obiects/<Stvoe)/search":+0 ..37 VISELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,u.emarsa.*t.owner 1d FROM social accounts saJOIN users uon v.id = sa.sociable_idJOIN teams t ..n<->1: on t.id = u.team_idWHERE u.team_id = 2 and sa.provider = 'hubspot':Used by opportunity sync strategies to paginate through deals/companies/contacts2. Service.php - Multiple search endpoints:MethadwEnd point/crm/v3/ob1eccs/scompanzes,contactss/search/crm/v3/ob1eccs/contacts/search/crm/v3/oojects/calts/searchsearchCallsForPeriodByPage()searchCallByRecordingURLToken(/crm/v3/objects/calls/searchImport calls by periodPadinated call searchFind call by recording URLselect * from social accounts whene id = 1499•select * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');select * from contacts where id = 85;select * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm_ provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:select * trom crm tield data where obnect 10 = 1505*SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):select * from reatures;select * from team features where team id IN (1):select * from team features where feature id IN (36):SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm provider 1d = 111751'43. PayloadBuilder.php generates payloads for:generate0pportunitySearchPayload() - Deal search for sync strategiesgenerateSearchContactsBvNamePayload - Contact name searchgeneratePhonesearchPay load() - Pnone searchgenerateSearchCallsByPeriodPayload - Call period searchgeneratebercaltspayload) - Call impon searchgenerateSearchCallByTokenPayload - Call by token searchKev Findinas1. No Retry-After header in search responses - Your debua loa contirms this. HubSpot doesn't send rate limit quidance in searchArlresponses,2. Rate limits are enforced server-side:• ten secondly rollina → 10 requests per 10 secondssecondly → 1 request per second (for search endpoints)3. Current retry logic (in Service. php lines 1723-1727):h phoGretrvifter = iccet/<recnonsef!nolicvName'l)S& Sresponsel 'policyName'] == self::TEN_SECONDLY_ROLLING_POLICY? self::TEN_SECONDLY ROLLING LIMIT // 10 seconds:1:1Default 1 second"Ssiud & "HURSPOT WERHOOK SYNCI# Steam = Jiminnv Models Team: • find(2)•# $feature = Feature::query(->where('slug', $slug)->firstO:# TeanFeature::quenv()->create(['feature id' => Sfeature->aetId() Iteam id' => Steam->aetId@l)•RecommendationSince search APls don't return Retry-After headers, the current approach of usina policyName from the error response body is theonlv reliable method. The code chould.# hubsnot wehhook metricsl1. Parse the exception response body for policyName2. Use hardcoded delavs based on policy typeselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993' , '504•TEN SECONDLY ROLLING → wait 10 secondsSELECT * FROM opportunity contacts WHERE opportunity id = '414':sscount y (coarch limitc) & wait 1 cecondlSELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•select * from activities where crm confiquration id = 2:3. Add exponential backoff for repeated failuresAll search requests ao throuch the same pattern: direct HTTP POST to crm/v3/obiects/<type)/search endooints via Sthis->client-›aecinstance(→›aercllent(→reauesco.select settings from crm confiqurations where id = 11;< CodeC° AdantiveW Windsurf Toamc1155-5 /705 charc 25 line hreakc) UTF.8io 4 spaces...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4880
|
|
4879
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
return null;
// 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) {
...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4879
|
|
4878
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 • now100% <8•Thu 7 May 17:35:29Lukas Kovalik5:35 PM | Lukas/Stefka 121• 4:51...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4878
|
|
4877
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
PhostormProiectINavigarecodeFV faVsco.js?9 master k>D CrmObjects> • DecorateActivity>D Dummy> • Helpersv D HubspotAccountSyncStrate> D Actions0 ContactSyncStrateDDIe• U rielas• C Journal© RateLimitException.php© PaginationState.php© HubHubspotwebhookbatchsyncstrateav.ongnportopportunity8atch.onoImportBatchJobTrait.pho(c) Hubspot/Service.php X(C) PayloadBuilder.phoC) Companies.php© MatchActivityCrmData.phpC) CrmActivityService.phoCachedCrmServiceDecorator.php•Hubspot/....SyncCrmEntitiesTrait.php(C) Pipedrive/Service.php1Servicelnterface.ohd@ OpportunitvSvncTest.ohp10.02.22 VasilevMetadata23.11.23 Stamatovv _ Opporunilvsyncsrclass Service extends BaseService implements m48 A49 V1 X33 41A1715private function bulkAddPlaybackURLToDescriptionRequest(array $payload1733Concerns23.11.23 Stamatov221122 Ctamatal'reason' => $e->getMessageO.'retry' => Sretry.c) Hubspotlastmo23.11.23 Stamatov(C) HubspotlastMo23.11.23 Stamatovc) Hubspotlastmo23.11.23 Stamatov© HubspotLastMor 23.11.23 StamatovC) -uosootLastMo24.01.25 Papazovc) -uospotsindlest23.11.23 Stamatov1757if (Sretry) {1740 Greturn sthis->buLKAddPLaybackURLlobescript1onkequestspay LoadC. HubsnotSvncStr23.11.23 StamatovC HubspotWebhoc 24.01.25 Papazo• M Padinationi23.11.23 Stamatov© HubspotPaginati 23.11.23 St(c) PadinationConfit23.11.23 |(c) DadinationState21.11.23M ProcnectSoarchStr.21.11.23• M Pedic21.11.2317431return false:Cascade &7ICommand &lnetimes we have secondly rate limit error, then retry request afterv M ServiceTraits1749 Gtublic function searchCallßvRecordingURLToken(strina SolavbacklRLToken.]n OpportunitysyncT SyncCrmEntitiesT SyncFieldsTrait.Sendnoint = [URL_WITH_CREDENTIALS] HS_local (iminny@localhost] >iti accounts (jiminny@localhost]CascadeA console (PROD]# console [eu)A console [STAGING]Hubspot Rate LimitingHubSpot Search Rate+0 ..Tx: AutoSo jiminnySendpoint = Client:: BASE URL . "/crm/v3/obiects/Stvoe)/search":SELECT * FRUM crm_tieLd_data WhERE crm_layout_entity_1d IN (6494,6495,64%6,6497,0498,0499); m 06 A1 x6 ~Used by opportunity sync strategies to paginate through deals/companies/contactsISELECT2. Service.php - Multiple search endpoints:CONCAT(u.id, CASE WHEN .id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,u.emarsa.*Methodfind()End point/crm/v3/ob1eccs/scompanzes,contactss/searchNlanls cawaaeawecealaniaThett.owner 1d FROM social accounts sa/crm/v3/ob1eccs/contacts/searchJOIN users uon v.id = sa.sociable_idJOIN teams t .n<->1: on t.id = u.team_idWHERE u.team_id = 2 and sa.provider = 'hubspot':/crm/v3/oojects/calts/searchImport calls by periodPadinated call searchsearchCallByRecordingURLToken(/crm/v3/objects/calls/searchFind call by recording URL3. PayloadBuilder.php generates payloads for:select * from social accounts whenp id = 1499•generate0pportunitySearchPayload() - Deal search for sync strategiesselect * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');generateSearchContactsBvNamePayload - Contact name searchgeneratePhonesearchPay load() - Pnone searchgenerateSearchCallsByPeriodPayload - Call period searchselect * from contacts where id = 85;generatebertaltspayload) - Call impont searchgenerateSearchCallByTokenPayload - Call by token searchselect * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:Kev Findinas1. No Retry-After header in search responses - Your debua loa contirms this. HubSpot doesn't send rate limit quidance in searchArlresponses,select * trom crm tleld data where obnect 10 = 15057SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):2. Rate limits are enforced server-side:• ten secondly rollina → 10 requests per 10 secondssecondly → 1 request per second (for search endpoints)select * rrom reatu3. Current retry logic (in Service. php lines 1723-1727):select * rrom team teatures where team 1d in 00"select * from team features where feature id IN (36):h phoSHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm provider 1d = 111751'4Gretrvifter = iccet/<recnonsef!nolicvName'l)S& Sresponsel 'policyName'] == self::TEN_SECONDLY_ROLLING_POLICY? self::TEN_SECONDLY ROLLING LIMIT // 10 seconds:1:1Default 1 second"Ssiud & "HURSPOT WERHOOK SYNCI# Steam = Jiminnv Models Team: • find(2)•Recommendation# $feature = Feature::query(->where('slug', $slug)->firstO:TeamFeature:•querv()->create(['feature id' => Sfeature->oetId() Iteam id' => Steam->aetId@1)•Since search APls don't return Retry-After headers, the current approach of usina policyName from the error response body is theonlv reliable method. The code chould.# hubsnot wehhook metrico1. Parse the exception response body for policyName2. Use hardcoded delavs based on policy typeselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA••TEN SECONDLY ROLLING → wait 10 secondssscount y (coarch limitc) & wait 1 cecondl3. Add exponential backoff for repeated failuresAll search requests ao throuch the same pattern: direct HTTP POST to crm/v3/obiects/<type)/search endooints via Sthis->client-›select * from activities where crm confiquration id = 2:aecinstance(→›aercllent(→reauesco.select settings from crm confiqurations where id = 11;< CodeC° AdantiveW Windsurf Toam.1746.5 /1200 charc 21 line hroakc) UTF.8io 4 spaces...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4877
|
|
4876
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
return null;
// 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) {
...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4876
|
|
4875
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 • now100% <8•Thu 7 May 17:35:21Lukas Kovalik5:35 PM | Lukas/Stefka 121• 4:43....
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4875
|
|
4874
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 • now100% <8•Thu 7 May 17:35:11Lukas Kovalik5:35 PM | Lukas/Stefka 121• 4:33....
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4874
|
|
4873
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
return null;
// 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) {
...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4873
|
|
4872
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
PhostormProiect vcodeFV faVsco.jsD CrmObjects> • DecorateActivityDummy>• Helpersv D HubspotAccountSyncStrate>@ Actions0 ContactSyncStrate•DDI0© RateLimitException.php© PaginationState.php© Hubnportopportunity8atch.onoImportBatchJobTrait.pho(C) Hubspot/Service.php x (C) PayloadBuilder.phoC) Companies.php© MatchActivityCrmData.phpC) CrmActivityService.phoCachedCrmServiceDecorator.php•Hubspot/....SyncCrmEntitiesTrait.php(C) Pipedrive/Service.php1Servicelnterface.oho@ OpportunitvSvncTest.ohn• U rielas•J Journa.10.02.22 VasilevMetadatav Opporunilvsyncsru Concerns5.04.1926.10.23 Stamatov22 10 22 Ctamatal(C) HubspotlastMo(C) HubspotlastMo(C) HubspotlastMo(C) HuospotlastMo25.10.23 Stamatov25.10.23 Stamato25.10.23 Stamatov23.10.23 Stamato(c) -uosootLastмo(C) -uospotsindles23.10.23 Stamatov141022 CtonC HubspotSyncStr 24.10.23 StamatovHubspotWebhoc 24.10.23 Stev Padination24.10.23 Stamatov(C) HubsootPadinati(c) PadinationConfit26.10.23 Stamatov(c) DadinationState25.10.23 StM ProcnectSoarchStr.25.10.23 Stamatov• M Pedic12.06.24 Stamatovv M ServiceTraitsT. OpportunitysyncT SyncCrmEntitiesT SyncFieldsTrait.T WriteCrmTrait.pl> O Utils|> 0 Webhook|24.10.23 Stamatov25.10.23 Stamatov25.10.23 Stamatov© BatchSynccollectol 24.10.23 Stamatovc) BatchSyncRedisSer2410 22 Stamatolc) Client.phpC) ClosedDealStagess24.10.23 Stamatov2410 22 StamatolDealFieldsService.r2410 22 StamatolC) DecorateActivtv.on041022 Ctamatal© FieldDefinitions.php 24.10.23 StamatovC) FieldivoeConverte0410.22 Ctomotai1) HubsootclientintenC) Hubsoot TokenMan24.10./s samarov041022 CtomotniC) PavloadBuilder.ohv24.10.23 StamatovC) RemoteCrm@biect.24.10.23 Stamatov9) ResnonceNormalizeC) Service nhn@ SvncField Action nh12.06.24 StamatovC) SncRelatedActivits12.06.24 Stamatov12.06.24 Stamatovclass Service extends BaseService implements m48 A49 V1 V33 21 ^1571 61Dlic function searchCalls(Carbon $from, Carbon $to, string SactivityProvider$this->logger->info('[HubSpot] Search calls'. ['from' => $from->format( format: self::L0G_DATE_FORMAT).15/5'to' => $to->format( format: self::L0G_DATE_FORMAT).1577scalls =1579Spage = 1do <try{Spavload = Sthis->navloadBuilder->qenerateGetcalsPavloadsfrom.$Sresponse = Sthis->client->getInstance@)->getClient()->request(Self::CALLS_SEARCH_ENDPOINT,= SnavloadcatchExcention Sexcention)$this->logger->info('[HubSpot] Search calls failed', [1591= Sfrom->format & format: self.LOG DATE FORMAT).'to' => $to->format( format: self::LOG_DATE_FORMAT),=> Sexception->getMessage,$response = null;$responseResults = empty($response['results']) ? (] : Sresponse['resul$calls = array_ merge($calls, $responseResults):spage++} while (! empty(SresponseResults)):1602160316041L0treturn Scalls1606 GfLic function searchCallsForPeriodByPage(Carbon $from, Carbon Sto, int Spage16071arQube for IDE suggestions: Detect more security issues in your PHP files // Try SonarQube Cloud for free // Download SonarQube Server // Learn more // Don't ask again (today 10:25)Lukas sterka 121 • nov100% 2• Thu 7 May 17:35:07AskJiminnyReportActivityServiceTest -=custom.log=laravel.logA SF (jiminny@localhost]4 HS_local (iminny@localhost] >iti accounts (jiminny@localhost]Cascade# console [Pkol)# console [eu)A console [STAGING]Hubspot Rate LimitingHubSpot Search Rate+0 ..Tx: AutoSo jiminnySendpoint = Client:: BASE URL . "/crm/v3/obiects/<Stvoe)/search":SELECT * FRUM crm_tieLd_data WhERE crm_layout_entity_1d IN (6494,6495,64%6,6497,0498,0499); m | 06 A1 x6 ^Used by opportunity sync strategies to paginate through deals/companies/contactsw37 VISELECTE 50E 52.= 6o6365= 772. Service.php - Multiple search endpoints:CONCAT(u.id, CASE WHEN u.id = t.owner id THEN ' (owner)' ELSE •• END) AS user_id.u.emarMethodEnd pointsa.*/crm/v3/ob1eccs/scompanzes,contactss/searcht.owner 1d FROM social accounts saрeaкkequesse/crm/v3/ob1eccs/contacts/searchJOIN users uon v.id = sa.sociable_idJOIN teams t .n<->1: on t.id = u.team_idWHERE U.team_id = 2 and sa.provider = 'hubspot':/crm/v3/oojects/calts/searchImport calls by periodPadinated call search/crm/v3/objects/calls/searchFind call by recording URL3. PayloadBuilder.php generates payloads for:select * from social accounts whenp id = 1499•generate0pportunitySearchPayload() - Deal search for sync strategiesselect * from opportunities where team_id = 2and crm_provider_id IN ('51317301383');generateSearchContactsBvNamePavload - Contact name searchgeneratePhonesearchPay load() - Pnone searchgenerateSearchCallsByPeriodPayload - Call period searchselect * from contacts where id = 85;generatebercaltspayload) - Call impon searchgenerateSearchCallByTokenPayload - Call by token searchselect * from opportunities where team id = 2 order by id desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112select * from opportunities where team id = 2 and crm_ provider id = '55976759904': # 5112select * from opportunity contacts where opportunity id = 5117:Kev Findinas1. No Retry-After header in search responses - Your debua loa contirms this. HubSpot doesn't send rate limit quidance in searchAPI responsesselect * trom crm tield data where obnect 10 = 1505*SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):2. Rate limits are enforced server-side:• ten secondly rollina → 10 requests per 10 secondssecondly → 1 request per second (for search endpoints)select * tromi3. Current retry logic (in Service. php lines 1723-1727):select * rrom ream teatures where team 1d in 00"select * from team features where feature id IN (36):h phoSHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm provider 1d = 111751'1Gretrvifter = iccet/<recnonsef!nolicvName'l)S& Sresponsel 'policyName'] == self::TEN_SECONDLY_ROLLING_POLICY? self::TEN_SECONDLY ROLLING LIMIT // 10 seconds:1:1Default 1 second# Steam = Jiminnv Models Team: • find(2)•Recommendation# $feature = Feature::query(->where('slug', $slug)-›firstO;TeamFeature: quenyo->createdi' featune id!= Sfeature->aetId() Iteam id! = Steam->aetId@1)•Since search APls don't return Retry-After headers, the current approach of usina policyName from the error response body is theonlv reliable method. The code chould.# hubsnot wehhook metricsl1. Parse the exception response body for policyName2. Use hardcoded delavs based on policy typeselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '504SELECT * FROM opportunity contacts WHERE opportunity id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA••TEN SECONDLY ROLLING → wait 10 secondssscount y (coarch limitc) & wait 1 cecondl3. Add exponential backoff for repeated failuresAll search requests ao throuch the same pattern: direct HTTP POST to crm/v3/obiects/<type)/search endooints via Sthis->client-›select * from activities where crm confiquration id = 2:aecinstance(→›aercllent(→reauesco.select settings from crm confiqurations where id = 11;< CodeC° AdantiveWN Windsurf Toams 1581-75UTF.8io 4 spaces...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
4872
|